Skip to content

fix(otel): flush spans in-context to prevent span loss in long-running invocations#213

Draft
kakadiadarpan wants to merge 2 commits into
mainfrom
darpan/fervent-fermat-dc385c
Draft

fix(otel): flush spans in-context to prevent span loss in long-running invocations#213
kakadiadarpan wants to merge 2 commits into
mainfrom
darpan/fervent-fermat-dc385c

Conversation

@kakadiadarpan

@kakadiadarpan kakadiadarpan commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

Traces from long-running invocations (e.g. Vercel Workflows) are missing most spans from the first part of the run: the trace looks blank early on, with all step spans bunched up near the end. The work happens the whole time — the spans are genuinely lost.

Root cause

Two flaws stack up:

  1. Flush scheduling is wall-clock. The BatchSpanProcessor schedules flushes with a bare setTimeout (OTEL_BSP_SCHEDULE_DELAY, default 5s), which escapes the request's AsyncLocalStorage. When the timer fires, getVercelRequestContext() returns undefined.
  2. The exporter failed silently. VercelRuntimeSpanExporter treated a missing context as success: it acked the batch (ExportResultCode.SUCCESS) and dropped the spans without exporting anything.

Net effect: every timer flush before the end of the invocation throws its spans away. Only the final flush — triggered via waitUntil, which runs inside the request context — survives, so only the last few seconds of spans land. Verified: disabling the timer (OTEL_BSP_SCHEDULE_DELAY=600000) makes all spans survive on a long run.

Fix

  1. In-context periodic flush (CompositeSpanProcessor.onEnd): span.end() always runs inside the request's async context, so the processor now triggers a debounced forceFlush() from there — when ≥ OTEL_BSP_MAX_EXPORT_BATCH_SIZE (512) spans have ended or ≥ OTEL_BSP_SCHEDULE_DELAY (5000ms) has elapsed since the last flush. This replaces the escaping wall-clock timer as the effective flush driver. Vercel-only: off-platform there is no request context to lose, and behavior is unchanged.
  2. Per-trace span routing (VercelRuntimeSpanExporter): concurrent invocations on one function instance share the batch queue, and the runtime drops spans reported through a foreign invocation's channel — so flushing the whole queue through the ambient request's reportSpans loses the other invocations' spans (observed on warm instances). The CompositeSpanProcessor now captures the owning VercelRequestContext per trace id at root-span start, and the exporter groups each batch by trace id and ships every group through its owning context (falling back to the ambient one), deleting the entry after the invocation's final flush.
  3. The exporter never silently drops: batches that arrive without any request context, or groups whose reportSpans throws, are retained in a bounded buffer (OTEL_BSP_MAX_QUEUE_SIZE, default 2048; oldest dropped with a warning on overflow) and re-shipped with the next in-context flush.

Reusing the standard OTEL_BSP_* env vars keeps the existing knobs meaningful for both mechanisms.

Known residual

Spans retained after an invocation's final flush (their registry entry is gone by then) fall back to the next invocation's ambient context on a reused function instance, which the runtime may drop. Better than losing them silently, but confirmation from the runtime team on cross-invocation reportSpans semantics would settle it. A deeper fix (making reportSpans reachable without the request ALS) is runtime-side and out of scope here.

Testing

  • Unit tests for the debounced in-context flush (batch-size trigger, delay trigger, off-Vercel no-op) and the exporter routing/retain/re-ship/re-retain paths
  • type-check, eslint, full unit suite pass
  • Re-validate against a real long-running workflow deployment (all step spans land with default env)

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
otel-site Ready Ready Preview, Comment, Open in v0 Jul 7, 2026 3:29pm

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The @vercel/otel edge build fails its post-build size check: dist/edge/index.js is 191442 bytes, over the hard MAX_SIZES limit of 191000 in packages/otel/build.ts, causing the build to exit 1.

Fix on Vercel

Concurrent invocations on one instance share the BSP queue; flushing the
whole batch through the ambient request's channel drops foreign spans.
Also bump bundle size limits.
await this.forceFlush();
// The invocation is over; the channel can no longer accept spans and
// the entry would leak if the root span never ended.
deleteRequestContext(traceId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-trace request-context registry entry is deleted ~50ms into an invocation instead of at trace completion, so periodic in-context flushes for long-running invocations lose per-trace routing and drop spans on warm/concurrent instances.

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant