@nifrajs/otel
v3.1.0
Published
Distributed tracing for nifra - W3C traceparent/tracestate propagation + OpenTelemetry-semantic-convention spans via a pluggable exporter. Bridge to the OpenTelemetry SDK or log spans directly; no SDK bundled. Edge-safe.
Maintainers
Readme
@nifrajs/otel
Distributed tracing for nifra. The tracing() plugin continues (or starts) a W3C trace per
request, opens an OpenTelemetry-semantic-convention span, and exposes c.trace so you can
forward the trace to downstream services. One fail-open lifecycle owns parentage, identity, timing,
errors, final status, and exactly-once completion. Pluggable adapters project that observation into
the OpenTelemetry SDK, DevTools, private backends, or structured logs. No SDK bundled; edge-safe.
import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
const app = server()
.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
.get("/orders/:id", async (c) => {
// continue the trace into a downstream call:
const res = await fetch(`${INVENTORY_URL}/stock`, { headers: traceHeaders(c.trace) })
return { id: c.params.id, inStock: (await res.json()).ok }
})What it does per request
- Continues an inbound trace - parses the
traceparentheader; reuses itstrace-idand records the inbound span as the parent. No inbound header → starts a fresh trace. - Opens a span with HTTP semantic-convention attributes (
http.request.method,url.path,http.response.status_code, optionalservice.name), ended on response with duration + status (errorfor 5xx,okotherwise). - Exposes
c.trace({ traceId, spanId, parentSpanId?, sampled, traceparent }) - spreadtraceHeaders(c.trace)into any downstreamfetch/ctx.apicall to continue the trace. - Exposes
c.causality- a bounded, payload-free request node that survives durable command, event, workflow, projection, and repair seams.traceHeaders(c.trace, c.causality)forwards both conventions to a trusted downstream service. - Exposes
c.observation- integrations can start correctly-parented child observations or attach an adapter without rebuilding request lifecycle state. responseHeader: truealso setstraceparenton the response (browser/client correlation).
Durable causality and trust
Pass a durable recorder to append the request root before the handler runs. An explicitly configured recorder is correctness evidence, so its failure fails the request closed:
app.use(tracing({
exporter,
causality: {
recorder: durableGraphStore,
acceptInbound: (request) => verifyInternalServiceCredential(request),
},
}))Inbound causality headers are not trusted by default. This prevents an internet client from
injecting fake parents into another execution timeline. Supply acceptInbound only at an authenticated
service-to-service boundary; a false, thrown, or rejected decision starts a fresh graph. W3C
traceparent remains normal observability context, but a fresh durable execution id includes the
server-generated span id and is not copied from the untrusted header.
Use causalitySpanLink(context) when durable work opens a later observation. It creates a real OTel
link to the nearest observed causal ancestor and drops invalid/unanchored contexts rather than
inventing trace identity.
Adapters
Implement ObservationAdapter to send spans wherever you collect them:
interface ObservationAdapter {
onStart?(span: NifraSpan): void
onEnd(span: NifraSpan): void
}consoleSpanExporter()- logs each completed span as one structured line (dev / starting point).tracing({ adapters: [devtoolsAdapter, privateAdapter] })- fan out the same lifecycle; adapter failures are isolated and never alter the response.- OpenTelemetry SDK bridge - a ~10-line adapter maps
NifraSpanonto a real OTelSpanfrom aTracer(the attribute names already follow OTel conventions, so they pass straight through). Your app depends on@opentelemetry/*;@nifrajs/oteldoes not.
Connect your collector
Use traceparent and the built-in semantic attributes in every request span, then send spans to
your own collector through an exporter. Keep the package edge-safe by installing the OpenTelemetry
SDK only in apps that need that exporter.
For non-HTTP work, createObservationLifecycle() exposes the same state machine directly. Prefer
it over hand-rolling traceparent parsing, clocks, error status, or completion guards.
For AI agents
Start with LLM.md - this package's contract card (the exports you call + its footguns),
one cheap read instead of the whole corpus. For the wider framework: the repo's
AGENTS.md is the copy-paste quick reference, and
llms-full.txt is the full machine-readable corpus. Run nifra check as the
done-gate, or nifra mcp to give the agent live project tools.
