Tracing with OpenTelemetry
DaloyJS ships otelTracing(opts), a hook factory that produces a Hooks object compatible with @opentelemetry/api. It starts a SERVER-kind span per HTTP request, attaches the standard HTTP semantic-convention attributes, exposes the span on ctx.state for handlers, and ends the span exactly once when the response is sent.
The framework does not depend on @opentelemetry/api. You pass any tracer that implements the minimal TracingTracer interface, so the same hook works on Node with the OTel SDK, on Workers with a custom exporter, or in tests with an in-memory fake.
- 01optionalExtract contextcontextFromRequest reads traceparent / B3
- 02onRequeststart SERVER span + http.request.method, url.path
- 03handlerctx.state.otelSpan for events & child spans
- 04onSendhttp.response.status_code, recordException on errors
- 05exactly oncespan.end()5xx escalates to setStatus(ERROR)
Quick start
That single hook gives every request:
http.request.method,url.path,url.scheme,server.address(host without port),server.port(when present),url.query,user_agent.originalset ononRequest.http.response.status_codeset ononSend.recordException+setStatus(ERROR)on thrown errors, andERRORescalation for any5xxresponse.- A guaranteed single
span.end()per request, even if bothonErrorandonSendfire.
Reading the active span in handlers
The active span is exposed on ctx.state.otelSpan (key configurable via stateKey). Use it to add events, child spans, or extra attributes from inside a handler:
Customizing span name and attributes
All extractors are optional. They are merged on top of the defaults so you only need to override what you care about.
Propagating upstream context
DaloyJS does not bundle a propagator. If you want parent-span continuation from traceparent / B3 headers, use contextFromRequest to wire your propagator's extract in:
End-to-end with Jaeger (OTLP)
The repository ships a runnable example plus a Jaeger service in the examples/observability/ Docker stack, so you can watch real spans land in a trace UI without writing any exporter code. Because otelTracing() only needs a tracer that matches the small TracingTracer interface, the example wires in a dependency-free OTLP/HTTP exporter (about 120 lines of web-standard fetch + crypto) that ships spans straight to Jaeger's OTLP receiver, no @opentelemetry/* SDK required.
1. Start Jaeger
2. Run the demo app
3. Generate traffic and open Jaeger
Open http://localhost:16686, pick the daloy-otel-demo service, and you will see one SERVER span per request: the /boom span flagged as an error with an exception event, the /slow span showing its real duration, and the traceparent request stitched into the upstream trace as a child span.
In production you usually swap the demo exporter for the real SDK, trace.getTracer("svc") from @opentelemetry/api backed by @opentelemetry/sdk-node and an OTLP exporter. The otelTracing() call does not change; only the tracer you pass in does.
Lifecycle and limitations
- Request outcomes. Matched routes, unmatched requests (
404/405), and OPTIONS preflight responses all end withhttp.response.status_codeon the same span. - No global side effects. The hook never touches
globalThis, never installs a propagator, and never imports an OTel SDK, it stays adapter-portable. - Single end. If a handler throws, the same span is marked errored and ended once during
onSend; lateronError/ repeatonSendinvocations are no-ops. - Composes with other hooks. Combine
otelTracing(...)withrequestId(...),secureHeaders(...), etc., DaloyJS merges global, group, and per-route hooks pipeline-style.