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.
Quick start
import { trace } from "@opentelemetry/api";
import { App, otelTracing } from "@daloyjs/core";
const tracer = trace.getTracer("my-service");
const app = new App({
hooks: otelTracing({ tracer }),
});That single hook gives every request:
http.request.method,url.path,url.scheme,server.address,url.query,user_agent.originalset onbeforeHandle.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:
app.route({
method: "POST",
path: "/orders",
operationId: "createOrder",
responses: { 201: { description: "created" } },
handler: async ({ state, body }) => {
const span = state.otelSpan as import("@daloyjs/core").TracingSpan | undefined;
span?.setAttribute("order.size", body.items.length);
span?.setAttributes?.({ "tenant.id": state.tenantId as string });
return { status: 201 as const };
},
});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.
otelTracing({
tracer,
spanName: (req) => `HTTP ${req.method} ${new URL(req.url).pathname}`,
attributesFromRequest: (req) => ({
"tenant.id": req.headers.get("x-tenant-id") ?? "unknown",
}),
attributesFromResponse: (res) => ({
"http.response.body.size": Number(res.headers.get("content-length") ?? 0),
}),
});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:
import { context, propagation, trace } from "@opentelemetry/api";
otelTracing({
tracer: trace.getTracer("my-service"),
contextFromRequest: (req) =>
propagation.extract(context.active(), req.headers, {
get: (headers, key) => headers.get(key) ?? undefined,
keys: (headers) => Array.from(headers.keys()),
}),
onSpanStart: (_req, span) => {
span.setAttribute("component", "daloy");
},
});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.
Tree-shake-friendly subpath
// Main barrel:
import { otelTracing } from "@daloyjs/core";
// Or, to keep your bundle minimal:
import { otelTracing } from "@daloyjs/core/tracing";