OpenTelemetry
A zero-config bridge that turns trace() spans into real OTel spans, records emits as span events, and publishes the W3C traceparent.
The core package ships an OpenTelemetry bridge that reconstructs real OTel spans from trace() activity and records POINT emits as events on the active span. When an OTel SDK is installed it turns on automatically — no extra wiring — and when it isn't, it's a no-op you can safely leave on.
Optional peer
@opentelemetry/api is an optional peer dependency of @adonis-agora/diagnostics. The bridge module (and its OTel import) is loaded dynamically by the provider only when OTel is resolvable, so the emit/trace hot path stays OTel-free by default.
Auto-start
If @opentelemetry/api can be resolved — because your app has an OTel SDK, e.g. @adonisjs/otel or a manual NodeSDK setup — the bridge starts with the app and stops with it. Every trace() span is already an OTel span; there is nothing to install or call. With no SDK present the bridge is skipped entirely, silently, and OTel is never even loaded.
Opting out
Set otel: false in your diagnostics config and the bridge never starts, SDK or not:
import { defineConfig } from '@adonis-agora/diagnostics'
export default defineConfig({
otel: false,
})Do that when you want emit/trace to stay OTel-free on a hot path even though an SDK is installed. See the config reference for the other keys.
What the bridge does
It subscribes to every agora:<lib>:<event> base channel and its five span sub-channels (current and future, via the registry), and reconstructs one OTel span per agora spanId:
startopens a span namedagora.<lib>.<event>, parented tocontext.active()at publish time, with attributesagora.lib,agora.event,agora.trace_id(when present), and flattened scalar payload fields asagora.payload.<key>.end(sync) /asyncEndclose the span OK, recordingagora.result(when scalar) andagora.duration_ms.errorrecords the exception and closes the span with statusERROR.
POINT events (emit) are added to the active span as span events named agora.<lib>.<event>, carrying agora.lib, agora.event and the flattened payload, timestamped with the envelope's ts. With no active span there is nothing to attach them to, so they are dropped. (Disable this with recordPointEvents: false.)
Events a lib-specific consumer has claimed are skipped by default — the claiming lib already records its own typed observation, so bridging them again would duplicate every such event. Override with recordClaimed: true.
Zero-overhead without an SDK
Every @opentelemetry/api call resolves to a no-op span/tracer when no SDK is registered, so the bridge still subscribes but produces nothing. The tracer is resolved lazily on each start, so a late-registered SDK wins.
Bridge options
If you start the bridge manually (see below), start(opts) / new DiagnosticsOtelBridge(opts) accept (BridgeOptions):
| Option | Type | Default | Description |
|---|---|---|---|
tracerName | string | @adonis-agora/diagnostics | Name passed to trace.getTracer(...). |
recordPointEvents | boolean | true | Record POINT emits as events on the active span. |
maxOpenSpans | number | 10000 | Cap on open (un-ended) spans; the oldest is force-ended when exceeded. |
recordClaimed | boolean | false | Bridge events whose lib:event is claimed by a lib-specific consumer. Default skips them so they aren't recorded twice; true bridges everything. |
The open-span cap is a safety valve: if a trace() span never observes an end/error (e.g. a process killed mid-operation), the oldest span is force-ended with an ERROR status rather than leaking memory.
Manual control
You rarely need this — the provider handles it — but the bridge is exported from the @adonis-agora/diagnostics/otel subpath:
import { start, stop, DiagnosticsOtelBridge } from '@adonis-agora/diagnostics/otel'
// Idempotent module-level singleton:
start({ tracerName: 'my-app', recordPointEvents: true })
// …
stop()
// Or manage an instance yourself:
const bridge = new DiagnosticsOtelBridge({ maxOpenSpans: 1000 })
bridge.start()
console.log(bridge.openSpanCount)
bridge.stop()start() is idempotent — a second call with no stop() in between returns the existing singleton.
The traceparent slot
When the bridge starts it also publishes the current active span as a W3C traceparent string on a cross-copy-stable global slot, Symbol.for('@agora/otel:traceparent'). This lets a durable/remote engine continue the trace on a worker with zero config — it reads the slot structurally and no-ops when the slot is absent:
import { otelTraceparent } from '@adonis-agora/diagnostics/otel'
// Wire it into a durable engine so remote steps continue the trace:
engine.configure({ traceparent: () => otelTraceparent() })otelTraceparent() uses the globally-registered OTel propagator (propagation.inject), so a standard W3C setup needs no extra config; with no SDK registered it returns undefined and never throws. @adonis-agora/diagnostics/otel also exports publishTraceparentSlot() / clearTraceparentSlot() and the TRACEPARENT_SLOT symbol for manual control.
Relationship to trace correlation
The bridge produces OTel spans; the traceId on each envelope (auto-filled from @adonis-agora/context) is a separate, ecosystem-level correlation id surfaced as the agora.trace_id attribute. The two are complementary: OTel gives you the trace tree, traceId ties an event back to the originating request even on a path with no active OTel span (e.g. a background worker).
Transports
Fan diagnostics events out across processes — pick a transport (Redis or @adonisjs/queue) in config/diagnostics.ts, exactly like a session or cache store.
Claim registry
How a lib-specific watcher claims a diagnostics channel so the generic OTel bridge dedups — recording each event once, not twice.