Skip to content

Commit 1cb9ace

Browse files
authored
fix(tracing): flush root spans before shutdown cleanup to prevent orphan traces (#112)
Container restart (SIGTERM → timeout → SIGKILL) killed the process before root spans were ended. Child spans already flushed to OTLP survived in Tempo without their parent root span, creating orphan `?` traces. Fix: - Add flushRootSpans() to CdpSession — ends all tab + session root spans immediately - Call flushAllRootSpans() as Step 0 in shutdown() BEFORE slow replay/browser cleanup - Increase shutdown timeout from 10s to 25s (aligned with replay flush timeouts) - SpanProto.end() pushes to OTLP exporter buffer — spans survive even if killed mid-cleanup
1 parent f50b8ee commit 1cb9ace

File tree

4 files changed

+42
-1
lines changed

4 files changed

+42
-1
lines changed

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ const program = Effect.fn('browserless.main')(function* () {
2525
const shutdown = (_reason: string, code: number) =>
2626
Effect.runPromise(
2727
Effect.tryPromise(() => browserless.stop()).pipe(
28-
Effect.timeout('10 seconds'),
28+
// 25s — enough for session cleanup (replay flush + browser close) + OTLP export.
29+
// Docker stop_timeout must be > this (set to 30s in infra config).
30+
// Previous 10s timeout killed the process during replay flush, losing root spans.
31+
Effect.timeout('25 seconds'),
2932
Effect.catch(() => Effect.void),
3033
),
3134
).finally(() => process.exit(code));

src/session/cdp-session.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,26 @@ export class CdpSession {
151151
return this.targets.size;
152152
}
153153

154+
/**
155+
* End all root spans immediately — called during graceful shutdown BEFORE
156+
* slow cleanup (replay flush, browser.close). Pushes spans to the OTLP
157+
* exporter buffer so they survive even if the process is killed mid-cleanup.
158+
*
159+
* SpanProto.end() pushes to the server exporter via export(). Tempo
160+
* deduplicates by spanId, so the safety-net end() calls in ensuring/
161+
* destroyEffect are no-ops in practice (same spanId, already ingested).
162+
*/
163+
flushRootSpans(): void {
164+
const now = BigInt(Date.now()) * 1_000_000n;
165+
for (const [, tab] of this.tabs) {
166+
tab.span.end(now, Exit.void);
167+
}
168+
if (this.sessionSpan) {
169+
this.sessionSpan.end(now, Exit.void);
170+
this.sessionSpan = null;
171+
}
172+
}
173+
154174
// ─── Layer Construction ───────────────────────────────────────────────
155175

156176
/**

src/session/session-coordinator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ export class SessionCoordinator {
3636
videoMgr?.setVideoEncoder(this.videoEncoder);
3737
}
3838

39+
/**
40+
* End all root spans across all active CdpSessions immediately.
41+
* Called during graceful shutdown BEFORE slow cleanup so spans reach
42+
* the OTLP exporter buffer before the process is killed.
43+
*/
44+
flushAllRootSpans(): void {
45+
for (const [, cdpSession] of this.cdpSessions) {
46+
cdpSession.flushRootSpans();
47+
}
48+
}
49+
3950
/**
4051
* Check if replay is enabled (always true — replay uses external server).
4152
*/

src/session/session-lifecycle-manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,13 @@ export class SessionLifecycleManager {
393393
async shutdown(): Promise<void> {
394394
this.log.info('Closing down browser sessions');
395395

396+
// Step 0: Flush all root spans IMMEDIATELY — before any slow cleanup.
397+
// This pushes session + tab root spans to the OTLP exporter buffer so
398+
// they survive even if the process is killed during replay/browser cleanup.
399+
// Without this, container restart (SIGTERM → timeout → SIGKILL) loses
400+
// in-memory root spans, creating orphan `?` traces in Tempo.
401+
this.sessionCoordinator?.flushAllRootSpans();
402+
396403
// Stop watchdog
397404
if (this.watchdogFiber) {
398405
await Effect.runPromise(Fiber.interrupt(this.watchdogFiber));

0 commit comments

Comments
 (0)