fix(otel): flush spans in-context to prevent span loss in long-running invocations#213
Draft
kakadiadarpan wants to merge 2 commits into
Draft
fix(otel): flush spans in-context to prevent span loss in long-running invocations#213kakadiadarpan wants to merge 2 commits into
kakadiadarpan wants to merge 2 commits into
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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); |
Contributor
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
BatchSpanProcessorschedules flushes with a baresetTimeout(OTEL_BSP_SCHEDULE_DELAY, default 5s), which escapes the request'sAsyncLocalStorage. When the timer fires,getVercelRequestContext()returnsundefined.VercelRuntimeSpanExportertreated 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
CompositeSpanProcessor.onEnd):span.end()always runs inside the request's async context, so the processor now triggers a debouncedforceFlush()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.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'sreportSpansloses the other invocations' spans (observed on warm instances). TheCompositeSpanProcessornow captures the owningVercelRequestContextper 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.reportSpansthrows, 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
reportSpanssemantics would settle it. A deeper fix (makingreportSpansreachable without the request ALS) is runtime-side and out of scope here.Testing
type-check,eslint, full unit suite pass