Diagnostics
A zero-dependency diagnostics bus over node:diagnostics_channel — emit once, observe anywhere, with an OpenTelemetry auto-bridge.
@adonis-agora/diagnostics is a tiny, standard convention for @adonis-agora/* AdonisJS libraries to emit observability events over Node's built-in diagnostics_channel. A library calls emit('billing', 'invoice-paid', payload) — or wraps an operation in trace('authz', 'decision', fn) — and is done. Whoever wants to observe it subscribes. It costs essentially nothing when nobody is listening, and never throws.
The point: producers stay decoupled from consumers
The key idea is that the channel is neutral. A library that emits a diagnostic event doesn't know — and doesn't care — who reads it. Telescope is one consumer (the batteries-included one), but the same event can just as easily become an OpenTelemetry span, feed an APM tool, drive a custom logger, fan out to other processes over a transport, or back an assertion in a test. See Consumers.
This is why the bus is a separate, dependency-free package: if emitting required Telescope (or OTel, or Redis), every producing library would be coupled to it. Instead, producers depend only on this thin layer, and observability is opt-in on the consumer side.
The model
Two emit surfaces, both over node:diagnostics_channel, both free when unobserved:
- POINT events —
emit(lib, event, payload)publishes oneDiagnosticEventonagora:<lib>:<event>. Use it for "something happened" facts. - SPANS —
trace(lib, event, fn)runsfnand publishes start / end / error (plus async phases) onagora:<lib>:<event>:<phase>, with timing and a sharedspanId. Use it for "this operation took N ms and succeeded/failed".
Every channel name shares the prefix agora and carries a standard envelope:
interface DiagnosticEvent<TPayload = unknown> {
v?: number; // SCHEMA_VERSION (1) — observers treat absent as v1
ts: number; // Date.now() at publish time
lib: string; // the <lib>, e.g. "billing"
event: string; // the <event>, e.g. "invoice-paid"
traceId?: string; // auto-filled from @adonis-agora/context when present
payload: TPayload; // your library-defined data
durationMs?: number; // wall-clock duration of the operation, when known
}Why diagnostics_channel?
It's a Node built-in, so the bus has zero runtime dependencies. Channels are process-global and resolved by name, so a producer and an observer in different packages — that never import each other — meet on the same channel. And emit gates on channel.hasSubscribers: with no observer attached, the envelope is never even built, so libraries can emit by default and pay nothing in production.
Quickstart
Install and configure:
node ace add @adonis-agora/diagnosticsEmit an event from anywhere — it's published only when something is subscribed:
import { emit } from '@adonis-agora/diagnostics'
emit('billing', 'invoice-paid', { invoiceId: 'inv_123', amount: 4200 })Observe it from any consumer — the simplest being onDiagnostic, registered in start/diagnostics.ts. See Getting Started for the full flow and trace correlation.
Explore
One package, @adonis-agora/diagnostics. Cross-process fan-out is a config choice, not an extra package — see Transports.
Getting started
The core bus — emit(), trace(), onDiagnostic(), the capability protocol, and the OTel auto-bridge.
Consumers
Subscribe and observe — built-in consumers, custom subscribers, and test assertions.
Transports
Fan events out across processes — pick a Redis or @adonisjs/queue transport in config/diagnostics.ts.
OpenTelemetry
The auto-bridge — every trace() span becomes a real OTel span when an SDK is present.
Where to go next
Getting Started
Install, configure, emit POINT events and trace() spans, and auto-fill traceId from @adonis-agora/context.
Consumers
Observe events with onDiagnostic, write your own subscriber, and assert on emits in tests.
OpenTelemetry
The zero-config bridge that turns trace() spans into real OTel spans and publishes the W3C traceparent.