OpenTelemetry
OpenTelemetry at the Askr server boundary, with validated input and explicit failure states.
Example
Create the function-first telemetry bridge after installing an application-owned OpenTelemetry provider and wrap work at semantic boundaries.
import { createTelemetry } from '@askrjs/otel';
const telemetry = createTelemetry({ tracerName: 'project-api' });
return telemetry.request({ requestId }, () => router.handle(request));Instrumentation boundary
`@askrjs/otel` only talks to the standard `@opentelemetry/api` peer dependency — it never bundles an SDK, span processor, exporter, or vendor backend. Call `createTelemetry(options)` once during app composition, and every span or log call routes through whatever provider your app registers; without one, the no-op implementation from the standard API absorbs the calls silently. That keeps this package usable in a test suite or a small app with zero telemetry infrastructure attached.
Spans and fields
The returned `Telemetry` object exposes one helper per lifecycle stage — `request`, `routeMatch`, `loader`, `action`, `apiOperation`, `queryPrefetch`, `ssrRender`, `viteDocument` — each wrapping a unit of work and recording it under a fixed `askr.*` `TelemetryOperation` name. `span(operation, fields, work)` is the general form underneath all of them, but `operation` isn't a free-form string — it's restricted to that same closed set of `askr.*` operation names, so there's no custom-operation escape hatch here despite `span` being the lower-level call. `TelemetryFields` is a narrow, typed shape — `requestId`, `traceId`, `route`, `action`, `status`, `durationMs` — so there's no open-ended bag of attributes to accidentally overload a span with; per-field redaction goes through `sanitizeField`, capped further by `maxFieldLength` (256 characters by default). Exceptions are the one thing NOT captured by default: `createTelemetry` only exports an exception on a span if you supply `sanitizeException` and it returns a value — without it, a thrown error inside `telemetry.span(...)` produces no exception record at all, not an auto-redacted one.
Redaction
Because `TelemetryFields` is a closed interface rather than `Record<string, unknown>`, there's no field for request bodies, submitted form values, cookies, tokens, or arbitrary user attributes — you literally cannot pass them in through the typed API. The `route` field is meant to hold a route pattern like `/projects/{projectId}`, not the raw, potentially sensitive URL a user actually requested. This is what keeps span data safe to forward to third-party backends by default, without a separate scrubbing pass.
Exporter setup
Choosing and configuring an actual OpenTelemetry SDK, processor, and exporter is application work, done in your composition root alongside `createTelemetry`. `TelemetryOptions` lets you customize the tracer's `tracerName`/`tracerVersion`, plug in a `logger` callback to mirror span-level events into your own structured logs, and override `now` for deterministic timing in tests. `extract`, `inject`, and `withContext` round out the object for propagating trace context across process or transport boundaries once a real exporter is wired up.