Skip to content

feat(observability): experimental_registerOpenTelemetry() bridges the OpenTelemetry API to Workers tracing - #2752

Merged
mattzcarey merged 1 commit into
mainfrom
feat/observability-otel-api
Oct 28, 2026
Merged

mattzcarey merged 1 commit into
mainfrom
feat/observability-otel-api

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Adds experimental_registerOpenTelemetry() to agents/observability, experimental and undocumented for now. One call at module scope turns @opentelemetry/api spans (the MCP SDK, AI SDK v6 experimental_telemetry, your own tracers) into Workers trace spans. They nest with Agents SDK spans and the automatic fetch/KV/D1/DO spans, and export through observability.traces.destinations.

import { experimental_registerOpenTelemetry } from "agents/observability";

experimental_registerOpenTelemetry();

Why:

How it works:

  • Every span opens with tracing.startActiveSpan and keeps an AsyncLocalStorage.snapshot() taken while it is active. context.with(trace.setSpan(ctx, span), fn) runs fn inside that snapshot, so platform and child spans nest under it.
  • In Durable Object constructors and blockConcurrencyWhile, tracing.getActiveSpan() returns undefined but native spans still parent to the request; there the bridge lets the runtime pick the parent instead of refusing.
  • Things Workers spans can't represent are refused (non-recording span) or dropped, never approximated: remote parents, root: true, explicit start times, parents from another request, links, array attributes. Span kind isn't recorded.
  • Registration is idempotent. It steps aside if OpenTelemetry is already fully registered (a user SDK, or Workers natively later), and throws on a half-registered setup.
  • @opentelemetry/api becomes a dependency of agents. The main agents entry doesn't load the bridge.
flowchart TD
  A["POST (invocation)"] --> B["tools/call get-weather (MCP SDK, OTel)"]
  B --> C["kv_get (platform)"]
  B --> D["look up forecast (user OTel span)"]
  E["invoke_agent (Agents SDK)"] --> F["chat model"] --> G["model work (OTel)"] --> H["kv_get"]
Loading

Design rationale and what it records and refuses: design/rfc-opentelemetry-bridge.md. No user docs yet; Agents SDK spans stay on native tracing.

Unit tests ran (src/node-tests/otel/). pnpm run test:otel-runtime (opt-in, not in CI) packs the package, runs wrangler dev 4.153 with AI SDK v6, and checks the span trees through Local Explorer.


Devin Review

Deployed to the agents account and checked with `cf o11y telemetry query`: MCP client and server spans, AI SDK spans, user spans and a renamed root span all nest under the request, with the MCP server side carried into the same trace over a service binding.

@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 10c0c08

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think Patch

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

@devin-ai-integration devin-ai-integration 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.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +230 to +238
const currentTraceId = traceIdOf(current);
if (
this.#traceId !== undefined &&
currentTraceId !== undefined &&
this.#traceId !== currentTraceId
) {
return undefined;
}
return this.snapshot;

@devin-ai-integration devin-ai-integration Bot Oct 27, 2026 •

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.

🔴 Cross-request spans acquire the wrong parent

When two requests share a trace ID, reentry accepts a span from the earlier request. Its snapshot moves subsequent native work into that request, attaching spans to the wrong invocation.

Learn more

A span opened with startSpan captures an async-context snapshot. Later, context.with uses reentry to activate that span for native work. This guard only compares native trace IDs. Separate requests in a distributed trace can share an ID, and supported older runtimes expose no span IDs at all. Reentry then runs the second request's work in the first request's captured native context.

Example: Request A opens span a and retains its context. Request B, another invocation of the same distributed trace, runs context.with(trace.setSpan(context.active(), a), () => fetch(...)). The fetch becomes a child of A's span rather than executing in B's native tracing context.

Recommended fix: Associate captured spans with a request-scoped identity that survives native span nesting, and reject cross-invocation reentry even when trace IDs match or are unavailable. Apply that identity check to both Bridge.place and WorkersContextManager.with; add same-trace and no-span-ID cross-invocation tests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +56 to +65
if (
contextOwner !== undefined &&
traceOwner !== undefined &&
!ownsContext &&
!ownsTrace
) {
diag.info(
"agents/observability: OpenTelemetry is already registered; registerOpenTelemetry() left it in place."
);
return "deferred";

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.

🟡 Incompatible registrations silently lose spans

When another API version owns both globals, registerBridge defers without checking compatibility. Instrumentation using an incompatible API copy then records no spans instead of receiving an error.

Learn more

OpenTelemetry registrations reside in a versioned global registry. This path detects an existing context manager and tracer provider but does not check the registry's version before declaring them usable. If this API copy cannot read the existing registrations, its instrumentation sees no working provider even though registration returned normally. The version-mismatch test covers only an empty registry, not a fully registered one.

Example: A compatible-looking global key contains version 1.0.0 plus an existing manager and tracer provider. A Worker using this package's 1.9.0 API calls registerOpenTelemetry(); registration defers, but that API cannot consume the older globals.

Recommended fix: Check registry compatibility before the fully-registered deferral branch, using the API's actual compatibility semantics rather than merely comparing raw version strings. Exercise the mismatch with both globals populated.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread design/rfc-opentelemetry-bridge.md Outdated
@@ -0,0 +1,156 @@
# RFC: OpenTelemetry API on Workers tracing

Status: proposed

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.

🔍 Bridge RFC remains undecided

The RFC remains proposed and says its decision is pending, although the bridge is implemented. The design workflow calls for a decision before implementation and an updated living design record.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2752

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2752

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2752

hono-agents

npm i https://pkg.pr.new/hono-agents@2752

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2752

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2752

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2752

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2752

commit: 10c0c08

@agent-think

agent-think Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

🔴 agents import sizes: 1 entry point over threshold

Entry point Exports Largest gzip change Size now
🔴 agents/observability 3 resized, 1 new up to +1.1 KiB (+416.99%) 1.3 KiB
Changed exports (4)
Import Gzip change Size now
🔴 agents/observability#channels +1.1 KiB (+416.99%) 1.3 KiB
🔴 agents/observability#genericObservability +1 KiB (+227.66%) 1.5 KiB
🔴 agents/observability#subscribe +1 KiB (+329.32%) 1.4 KiB
🆕 agents/observability#experimental_registerOpenTelemetry — 5.8 KiB
How this works

Each 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 d69277b5 → 10c0c08a · workflow run · reported by agent-think[bot]

@devin-ai-integration devin-ai-integration 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.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 2 new potential issues.

Devin Review

Comment on lines +183 to +187
return parent === undefined &&
!options.root &&
options.startTime === undefined &&
parentContext.getValue(AMBIENT) === true
? { in: undefined }

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.

🟡 Constructor span links disappear silently

When a constructor span includes links, place returns before the link diagnostic. Native tracing drops those links without warning.

Learn more

A recording span normally reaches the options.links check in place, which reports dropped links through diag. Ambient constructor spans return earlier, so the same unsupported links are dropped without a diagnostic.

Example: In a constructor, tracer.startSpan("initialize", { links: [{ context: priorContext }] }) opens a native span. Its creation link is lost, but the diagnostic logger never receives the link warning.

Recommended fix: Run the creation-link diagnostic for accepted ambient spans too, without changing the existing refusal rules or reporting links for spans that are refused.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +475 to +476
if (native === undefined) {
return scope?.context ?? ROOT_CONTEXT.setValue(AMBIENT, true);

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.

📝 Info: Outside-request context has a new identity

Outside a request, context.active() no longer equals ROOT_CONTEXT. Code comparing contexts by identity observes a change, though its active span remains undefined.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

… OpenTelemetry API to Workers tracing

Add experimental_registerOpenTelemetry() to agents/observability (undocumented
for now). It registers a global OpenTelemetry context manager and tracer
provider whose spans are Workers native spans, so OTel-instrumented libraries
(the MCP SDK, AI SDK v6 experimental_telemetry, user tracers) nest with Agents
SDK spans and the automatic fetch/KV/D1/DO spans and export through
traces.destinations.

Every span opens with tracing.startActiveSpan and keeps an async-context
snapshot taken while it is active; context.with(trace.setSpan(ctx, span))
re-enters it. Spans native tracing cannot represent faithfully (remote
parents, new roots, explicit start times, parents from another request) are
non-recording. Registration is idempotent, steps aside when OTel is already
fully registered, and throws on a partial setup.
@mattzcarey
mattzcarey force-pushed the feat/observability-otel-api branch from 00bd618 to 10c0c08 Compare October 28, 2026 15:50
@mattzcarey mattzcarey changed the title feat(observability): registerOpenTelemetry() bridges the OpenTelemetry API to Workers tracing feat(observability): experimental_registerOpenTelemetry() bridges the OpenTelemetry API to Workers tracing Oct 28, 2026

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

Devin Review

"agents": minor
---

Add `experimental_registerOpenTelemetry()` to `agents/observability`, which records spans from OpenTelemetry-instrumented libraries as Workers trace spans. Experimental and subject to change.

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.

🟡 Release note loses required documentation link

The experimental_registerOpenTelemetry() changeset has no documentation link. The changeset rule requires one, and the bridge's RFC is available to link.

Learn more

Package changesets must use a one-line description with a link to relevant documentation, as specified in changeset guidance. The new description is one line but removes its link. The user-facing OpenTelemetry section was removed, so reusing its URL would leave a dead anchor.

Example: Release notes generated from this changeset name the experimental API but give readers no linked explanation of its supported behavior.

Recommended fix: Link the available bridge RFC from this single-line changeset, or add and link an appropriate user-facing document if the experimental release warrants one.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
export { genericObservability } from "./emitter";
export type { Observability, ObservabilityEvent } from "./emitter";
export { experimental_registerOpenTelemetry } from "./otel";

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.

🔍 PR example names the wrong export

The PR description still uses registerOpenTelemetry() and links to a removed documentation section. Its example does not match the exported experimental_registerOpenTelemetry(); update the description before publishing.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@mattzcarey
mattzcarey merged commit 34cbd30 into main Oct 28, 2026
29 checks passed
@mattzcarey
mattzcarey deleted the feat/observability-otel-api branch October 28, 2026 16:04
@github-actions github-actions Bot mentioned this pull request Oct 28, 2026
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