Skip to content

Export Workers user spans over OTLP - #7686

Draft
danlapid wants to merge 12 commits into
mainfrom
dlapid/user-tracing-otlp
Draft

danlapid wants to merge 12 commits into
mainfrom
dlapid/user-tracing-otlp

Conversation

@danlapid

Copy link
Copy Markdown
Collaborator

Workers user spans reach a tracing backend today only through Streaming Tail Workers: the Workers Observability tail worker rebuilds them in JavaScript from tail events. Spans built that way are cut off when a Durable Object hands off work, carry coarse I/O-clock timestamps, and are sampled only after they already exist.

This gives the runtime what it needs to export user spans itself, as OTLP, the way other Cloudflare services already do:

  • an OTLP encoder for spans, written in Rust, behind the existing span submitter interface;
  • a tracer hook that lets the embedder open each invocation's root span, so the invocation and all of its user spans form one exported trace;
  • precise timestamps for user spans, which no longer reach user code directly.

workerd can also export on its own: a new tracing.otlp setting sends each invocation's spans to an OTLP/HTTP collector such as a local Jaeger. That makes Workers traces visible in local development without a tail worker.

Nothing changes unless an embedder installs the hook or the setting is configured.

Comment thread src/workerd/io/otlp-span-submitter.h Outdated

// Where an OtlpSpanSubmitter's spans go, and which spans it takes. Refcounted because the
// submitters of one invocation share it and may outlive the tracer.
class OtlpSpanExporter: public kj::Refcounted {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: unless OtlpSpanExporter needs to hand off refs to itself, it really doesn't need to extend public kj::Refcounted any more right?

Comment thread src/workerd/server/server.c++ Outdated
@@ -7907,6 +7919,12 @@ kj::Promise<void> Server::startServices(jsg::V8System& v8System,
}
}

if (config.getTracing().hasOtlp()) {
otlpExporter = kj::refcounted<OtlpExporter>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

kj::rc and kj::Rc instead?

Comment thread src/workerd/server/server.h Outdated

class OtlpExporter;
// Set when the config's `tracing.otlp` names a collector.
kj::Maybe<kj::Own<OtlpExporter>> otlpExporter;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

kj::Rc<OtlpExporter>?

Comment thread src/workerd/io/otlp-span-submitter.h
Comment thread src/rust/otlp/buffer.rs Outdated
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@wperron

wperron commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Oct 16, 2026
@wperron
wperron force-pushed the dlapid/user-tracing-otlp branch from 82bb3ec to 9faabe2 Compare October 26, 2026 19:13
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The generated output of @cloudflare/workers-types matches the snapshot in types/generated-snapshot 🎉

danlapid and others added 12 commits October 28, 2026 10:44
Workers user spans reach a tracing backend today only through
Streaming Tail Workers: the Workers Observability tail worker rebuilds
them in JavaScript from tail events. Spans built that way are cut off
when a Durable Object hands off work, carry coarse I/O-clock
timestamps, and are sampled only after they already exist.

This gives the runtime what it needs to export user spans itself, as
OTLP, the way other Cloudflare services already do:

- an OTLP encoder for spans, written in Rust, behind the existing
  span submitter interface;
- a tracer hook that lets the embedder open each invocation's root
  span, so the invocation and all of its user spans form one exported
  trace;
- precise timestamps for user spans, which no longer reach user code
  directly.

workerd can also export on its own: a new `tracing.otlp` setting sends
each invocation's spans to an OTLP/HTTP collector such as a local
Jaeger. That makes Workers traces visible in local development without
a tail worker.

Nothing changes unless an embedder installs the hook or the setting is
configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter is a stopgap copied from the Workers Observability tail
worker. Record why it is unsatisfying and that it can be removed once
every binding talks to its backend over JSRPC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some bindings (D1, AI, Images, Vectorize) reach their backend with an
internal fetch to a placeholder host. Those fetch spans were dropped
when they ended, but by then their id had already been handed to the
backend as its parent, so any spans the backend exported were orphaned
at the top of the trace.

Decide when the fetch starts instead: a fetch to one of these hosts
opens no user span, and the binding's own span (such as d1_run) becomes
the subrequest's parent. Matching by host is still a stopgap until
every binding talks to its backend over JSRPC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An uncaught exception reported for the invocation went only to the
streaming tail, so the exported root span had the exception outcome
but no exception event and no status message. Record it on the root,
as tail-built traces did. An exception reported while one of the
worker's own spans is active belongs to that span and is left off the
root.

When an exception supplies the status message, still derive
error.type, so every ERROR span carries one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenTelemetry spec doesn't care about the order of start times,
neither does the waterfall view in the dashboard.
The http_request spans demonstrated creating spans outside the Worker
request lifecycle, but are unnecessary for this PR.
- Add the `InstrumentationScope` to the proto
- Set the name to `workers-runtime` to match the telemetry sdk name
Keep shared exporter ownership in Rc handles rather than requiring\nintrusive Refcounted inheritance. Preserve the unique server control\nhandle and collector unlinking during shutdown.
Regenerate the crate_universe graph for prost against the current main dependency graph. Verify with repinning disabled and the focused OTLP and tracing tests.
@wperron
wperron force-pushed the dlapid/user-tracing-otlp branch from 122e1d1 to f727d85 Compare October 28, 2026 15:21
@wperron
wperron requested a review from jasnell October 28, 2026 15:25
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.

4 participants