Repository navigation
feat(observability): record Agents SDK spans through the OpenTelemetry API - #2753
mattzcarey wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 03119ff The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| if (settled) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟡 Agent spans disappear after telemetry reset
After context.disable() and trace.disable(), ensureOpenTelemetry() skips registration because settled remains true. Later Agent spans no longer reach Workers tracing.
Learn more
The bridge can re-register after both OpenTelemetry globals are disabled, but automatic registration currently runs only once per isolate. Once a request has registered it, settled prevents any further checks. Disabling the global context manager and tracer provider removes the bridge from the OTel registry, so subsequent requests cannot recover automatic tracing.
Example: A Worker records an Agent span, resets its OTel globals with context.disable() and trace.disable(), then handles another Agent request. The second request does not re-register the bridge and its spans are lost.
Recommended fix: Track whether the bridge still owns both global registrations, rather than treating the first attempt as permanently settled. Re-run registerBridge when both registrations have been removed, and cover disable-then-new-request with a test.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (nativeTracing === undefined) { | ||
| settled = true; | ||
| return; | ||
| } |
There was a problem hiding this comment.
| The Agents SDK records its own spans (agent lifecycle, `wrapAISDK`, Think) | ||
| through the OpenTelemetry API too, and registers the bridge automatically the | ||
| first time it records a span in a request. A Worker that runs an Agent, Think, | ||
| or `wrapAISDK` therefore needs no `registerOpenTelemetry()` call. Call it | ||
| yourself to record OpenTelemetry spans that can start before any Agents SDK | ||
| span, for example in a Worker that only serves `createMcpHandler`. | ||
|
|
||
| If you register your own OpenTelemetry SDK at module scope, the Agents SDK's | ||
| spans go to that SDK instead, like any other OpenTelemetry instrumentation. |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
🔴 agents import sizes: 1 entry point over threshold
Changed exports (57)
…and 7 more exports in the workflow artifact. How this worksEach runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement. Compared |
545e8aa to
684744c
Compare
fa5fab6 to
98cf061
Compare
684744c to
00bd618
Compare
…y API
AgentTracer (agent lifecycle spans, wrapAISDK, Think, GenAI spans) now takes
an OpenTelemetry Tracer, the global trace.getTracer("agents"), instead of
writing Workers native spans directly. The bridge from registerOpenTelemetry()
is the only code left on native tracing, and the first Agents SDK span inside
a request registers it, so an application's own module-scope OTel setup still
wins.
Span names and attributes are unchanged. Failures additionally set span status
ERROR with no description, and chat spans are CLIENT per the GenAI semantic
conventions.
98cf061 to
03119ff
Compare
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; |
There was a problem hiding this comment.
🔴 Module-scope spans preempt application tracing
When wrapAISDK starts a span before application OpenTelemetry setup, ensureOpenTelemetry claims the global provider at module scope. The application's later provider registration fails, leaving its tracing SDK inactive.
Learn more
The Agents tracer calls ensureOpenTelemetry() before starting any span in agentsTracer. A wrapped AI SDK call can start a span during Worker module evaluation; a synchronous mock model or precomputation needs no request I/O. At that point, getActiveSpan() is undefined. Without a request gate, registerBridge installs the bridge as the global context manager and tracer provider. An application that registers its own SDK later in the same module evaluation cannot install those globals, and the settled flag prevents reconsideration.
Example: A Worker calls a wrapped generateText with a local mock model at module scope, then registers its own OpenTelemetry SDK. The wrapper installs the bridge first, so the application's provider registration fails instead of owning later spans.
Recommended fix: Preserve deferral for module-scope spans while allowing constructors with no visible active span. Determine whether a Durable Object construction is in progress using an appropriate runtime signal or an explicit construction entry point rather than treating all undefined active spans alike.
Was this helpful? React with 👍 or 👎 to provide feedback.
| tracing.invocation((root) => | ||
| tracing.constructing(() => { | ||
| // The runtime reports no active span here. | ||
| expect(tracing.getActiveSpan()).toBeUndefined(); | ||
| tracer.withSpan("agent_initialization", {}, () => |
There was a problem hiding this comment.
|
Closing: Agents SDK spans stay on native Workers tracing. Through the bridge they carried the same names, attributes and nesting, but picked up the bridge's refusal cases, depended on getActiveSpan(), and installed a global OTel registration for every Agents user. The one real gain, error status on failures, is a small native change in a separate PR. #2752 (registerOpenTelemetry for third-party OTel libraries) is unaffected. |
Stacked on #2752. The Agents SDK's own spans (agent lifecycle,
wrapAISDK, Think, the GenAI spans) now go through the OpenTelemetry API, and the bridge from #2752 is the only code left on Workers native tracing. When Workers supports the OpenTelemetry API natively, deletingsrc/observability/otel/is the whole migration.AgentTracertakes an OTelTracer: the globaltrace.getTracer("agents").wrapAISDKneed noregisterOpenTelemetry()call. Agents SDK spans start in handlers and Durable Object constructors, after module evaluation, so an app's own module-scope OTel SDK still registers first; Agents SDK spans then go to that SDK.wobs/vega) key oninvoke_agent/chatnames,gen_ai.*attributes, and an error derived fromerror.type. The agent-metrics tail worker reads the same span-open, attribute and span-close events as before.ERROR, with no description and no exception event, so Vega's derived_erroris stillerror.type.chat {model}spans areCLIENTper the GenAI semantic conventions, which shows only under a non-Workers SDK, since Workers spans have no kind.Behavior changes to know about:
tracing.getActiveSpan(), 2026-09-25), Agents SDK spans are no-ops instead of native spans.Tests ran: the
agentsnode and workers projects, plus thethinkandai-chatworkers suites. I deployed probe Workers built from this branch and frommainto the agents account and queried them withcf o11y telemetry query. Across the same agent workload, the 45 Agents SDK spans carry identical names, parents, errors and 489 SDK-written attributes, and the agents view (agent, conversation, models, tokens, status) is the same. I also ran the same agent workload (generateTextwith a tool,streamText, a failing call) against packedmainand this branch inwrangler dev. Local Explorer showed the same 12 spans on both, with identical names, parents and attribute values.