Distributed Tracing
Deserve carries no OpenTelemetry SDK, no automatic span creation, and no trace context propagation. That boundary is drawn on purpose.
Why It Is Not Built In
A tracing SDK is a heavy, opinionated dependency. It pins exporter versions, owns the sampling policy, and decides how a span tree is shaped. Bundling one would break zero dependency and force a single vendor on every project, while every team already runs a different backend. One ships to Grafana Tempo, another to Jaeger, another to a hosted vendor, another to a homegrown collector.
So the decision is to stop at the data, not the transport. Deserve emits a complete request lifecycle through observability events, and each event already carries fields named after the OpenTelemetry semantic conventions. The event is the source of truth, and forwarding it to a tracing backend is a short subscription the developer owns.
What Ships, and What Does Not
These three sit outside the framework on purpose:
- Auto-instrumentation - Deserve does not wrap libraries or open spans for outbound calls. Each request emits one finished event, and a span is built from it in the listener.
- Trace context propagation - no
traceparentheader is read or written. A handler that needs distributed context reads the header throughctx.header('traceparent')and threads it onward. - Span hierarchy - events are flat, one per request, not a parent-child tree. Nested spans are assembled in the backend, or in the listener, from data the events provide.
What does ship is the data a span needs, already collected and named to match.
The Data Is Already There
Every request emits request:complete, and a request with status 400 or higher also emits request:error. Both carry the same envelope, and the metadata is the truth a span is built from:
timestamp- event creation time in epoch milliseconds, the span start anchor.durationMs- measured request duration, the span length.ip- resolved client IP, honoringtrustProxy.method,statusCode,url- the core request attributes.- OTel-aligned metrics -
route,serverAddress,serverPort,userAgent,requestSize,responseSize, forwarded only when known.
The field names follow the OpenTelemetry semantic conventions, so a map to span attributes is close to a rename. The full list lives in the Event Reference.
Building a Span From an Event
This listener turns each finished request into a span-shaped record and hands it to an exporter. Swap the exportSpan call for any backend client.
router.on((event) => {
// Build a span from each finished request
if (event.kind === 'request:complete') {
const m = event.metadata as {
method: string
url: string
statusCode: number
durationMs: number
route?: string
serverAddress?: string
serverPort?: number
userAgent?: string
}
exportSpan({
name: `${m.method} ${m.route ?? m.url}`,
startTimeUnixMs: event.timestamp,
durationMs: m.durationMs,
attributes: {
'http.request.method': m.method,
'http.response.status_code': m.statusCode,
'url.full': m.url,
'http.route': m.route,
'server.address': m.serverAddress,
'server.port': m.serverPort,
'user_agent.original': m.userAgent
}
})
}
})
await router.serve(8000)The attribute keys above are the OpenTelemetry HTTP span names, so the record drops straight into a tracing pipeline.
Continuing an Incoming Trace
Distributed tracing links spans across services through the traceparent header. Deserve does not parse it, so a handler that joins an existing trace reads the header from Context and carries it forward.
export async function GET(ctx: Context): Promise<Response> {
// Read upstream trace context when present
const traceparent = ctx.header('traceparent')
// Forward it on outbound calls
const upstream = await fetch('https://api.internal/data', {
headers: traceparent ? { traceparent } : {}
})
return ctx.send.json(await upstream.json())
}The same ctx.state used for sharing state holds a span ID across middleware and handler when one listener opens a span early and another closes it.
Where the Data Goes
The listener is the only seam, so the destination is a choice, not a constraint. Grafana, Jaeger, a hosted vendor, or a self-built collector all receive the same span record. Pair this with Request Logging for access logs and Error Reporting for failures, all from the one event bus.