You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I don’t believe Workers Observability supports user defined trace spans yet. Once it does, it should be possible to create a tracing Subscriber to collect spans.
For logging, this example shows using the tracing crate which should then emit logs that are captured by Workers Observability.
I looked into this too and had to fallback to the example that @kflansburg linked. When using tower_http::TraceLayer with axum it calls into the system's Time module, which compiles but panics at runtime on WASM.
I only looked very briefly, but I was unable to find a way to provide my own time's to the TraceLayer middleware via a formatter
It's possible with a custom executor and you also need to manually propagate context (doesn't really work out of the box like the rest of OTEL stack). I have it working reliably and sending data to honeycomb. The only issue (which seems unsolvable) is that all spans show the same start and end time because Cloudflare doesn't provide access to execution time within the platform. This means every span appears to have zero duration, making it impossible to see the actual execution time of individual functions.
The precondition @kflansburg named — "Once [Workers Observability supports
user-defined trace spans], it should be possible to create a tracing Subscriber
to collect spans" — shipped on 2026-06-16: custom spans via cloudflare:workerstracing.enterSpan.
It builds clean to wasm32-unknown-unknown, is clippy-clean, and worker-build
packages it into a deployable Worker whose bundle emits the expected glue
(import { tracing } from "cloudflare:workers", enterSpan, setAttribute, isTraced). It's two pieces:
Binding — enter_span (sync) and enter_span_async (for async
handlers, the callback returns a Promise that workerd awaits before
closing the span), plus a Span handle with set_* / is_traced. Safe, no unsafe: the sync form uses ScopedClosure::borrow_mut (the span callback
is immediate, so it can borrow non-'static state); the async form uses an
owned 'static closure since the promise outlives the call.
tracing_subscriber::Layer — forwards tracing::info! events and span
fields onto the active platform span as attributes, no per-call-site
changes.
The constraint worth discussing
The API is callback-scoped only — enterSpan(name, cb), no imperative startSpan/endSpan, no getActiveSpan. tracing span lifetimes are two
separate operations (on_enter / on_exit via a guard's Drop), so a Layer can't bridge an arbitrary span! to a platform span: at on_enter you'd have
to call enterSpan and not return from its callback until the later, separate on_exit. On a single-threaded Worker you can't suspend/resume that callback.
And durations have to come from the platform anyway (guest timer resolution is
clamped — the root of the zero-duration problem reported earlier in this
thread).
So the PoC splits it: span structure + timing come from closure-scoped enter_span wrappers (which an #[instrument]-style macro could generate); events + fields are forwarded transparently by the Layer.
Questions for the upstream shape
Land the binding split as worker-sys/src/types/tracing.rs + worker::tracing (mirroring the recent email encapsulation), with the Layer behind a tracing feature? Happy to open that PR.
Is there appetite to ask the runtime team for an imperative startSpan/endSpan or a getActiveSpan? That's the one addition that would
unlock a fully transparenttracingLayer (bridging span lifetimes, not
just events).
Activity
kflansburg commentedon Jan 3, 2026
I don’t believe Workers Observability supports user defined trace spans yet. Once it does, it should be possible to create a tracing Subscriber to collect spans.
For logging, this example shows using the tracing crate which should then emit logs that are captured by Workers Observability.
sagikazarmark commentedon Jan 4, 2026
Thanks @kflansburg! I saw that example, but I was wondering if a deeper integration was possible.
Feel free to close the issue (or keep it open for tracking, I couldn't find any other)
esotuvaka commentedon Jan 5, 2026
I looked into this too and had to fallback to the example that @kflansburg linked. When using
tower_http::TraceLayerwith axum it calls into the system's Time module, which compiles but panics at runtime on WASM.I only looked very briefly, but I was unable to find a way to provide my own time's to the TraceLayer middleware via a formatter
omarabid commentedon Jan 18, 2026
It's possible with a custom executor and you also need to manually propagate context (doesn't really work out of the box like the rest of OTEL stack). I have it working reliably and sending data to honeycomb. The only issue (which seems unsolvable) is that all spans show the same start and end time because Cloudflare doesn't provide access to execution time within the platform. This means every span appears to have zero duration, making it impossible to see the actual execution time of individual functions.
Butch78 commentedon Jun 17, 2026
The precondition @kflansburg named — "Once [Workers Observability supports
user-defined trace spans], it should be possible to create a tracing Subscriber
to collect spans" — shipped on 2026-06-16: custom spans via
cloudflare:workerstracing.enterSpan.I put together a working proof-of-concept of both halves (the binding and a
tracinglayer) so there's something concrete to design against:https://github.com/Butch78/cf-workers-rs-tracing
It builds clean to
wasm32-unknown-unknown, is clippy-clean, andworker-buildpackages it into a deployable Worker whose bundle emits the expected glue
(
import { tracing } from "cloudflare:workers",enterSpan,setAttribute,isTraced). It's two pieces:enter_span(sync) andenter_span_async(forasynchandlers, the callback returns a
Promisethat workerd awaits beforeclosing the span), plus a
Spanhandle withset_*/is_traced. Safe, nounsafe: the sync form usesScopedClosure::borrow_mut(the span callbackis immediate, so it can borrow non-
'staticstate); the async form uses anowned
'staticclosure since the promise outlives the call.tracing_subscriber::Layer— forwardstracing::info!events and spanfields onto the active platform span as attributes, no per-call-site
changes.
The constraint worth discussing
The API is callback-scoped only —
enterSpan(name, cb), no imperativestartSpan/endSpan, nogetActiveSpan.tracingspan lifetimes are twoseparate operations (
on_enter/on_exitvia a guard'sDrop), so aLayercan't bridge an arbitrary
span!to a platform span: aton_enteryou'd haveto call
enterSpanand not return from its callback until the later, separateon_exit. On a single-threaded Worker you can't suspend/resume that callback.And durations have to come from the platform anyway (guest timer resolution is
clamped — the root of the zero-duration problem reported earlier in this
thread).
So the PoC splits it: span structure + timing come from closure-scoped
enter_spanwrappers (which an#[instrument]-style macro could generate);events + fields are forwarded transparently by the
Layer.Questions for the upstream shape
worker-sys/src/types/tracing.rs+worker::tracing(mirroring the recentemailencapsulation), with theLayerbehind atracingfeature? Happy to open that PR.startSpan/endSpanor agetActiveSpan? That's the one addition that wouldunlock a fully transparent
tracingLayer(bridging span lifetimes, notjust events).
feat(worker): bindings for Cloudflare Workers custom spans (cloudflar…