Search docs

Jump between documentation pages.

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

ts
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.original set on beforeHandle.
  • http.response.status_code set on onSend.
  • recordException + setStatus(ERROR) on thrown errors, and ERROR escalation for any 5xx response.
  • A guaranteed single span.end() per request, even if both onError and onSend fire.

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:

ts
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.

ts
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:

ts
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 with http.response.status_code on 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; later onError / repeat onSend invocations are no-ops.
  • Composes with other hooks. Combine otelTracing(...) with requestId(...), secureHeaders(...), etc. — DaloyJS merges global, group, and per-route hooks pipeline-style.

Tree-shake-friendly subpath

ts
// Main barrel:
import { otelTracing } from "@daloyjs/core";

// Or, to keep your bundle minimal:
import { otelTracing } from "@daloyjs/core/tracing";