Consumers
Observe diagnostic events from anywhere — Telescope, OpenTelemetry, an APM, a logger, or your own subscriber.
A diagnostic event is just a message on a Node channel. Anything can read it. This page shows how to wire the common consumers — and how to write your own in a few lines.
The wildcard problem (and the registry)
Node's diagnostics_channel has no wildcard subscription: you can only subscribe to a channel by its exact name. Since channels are named per event (aviary:authz:decision, aviary:billing:invoice-paid, …), a generic consumer needs to know which channels exist. That's what the registry is for:
import diagnostics_channel from 'node:diagnostics_channel';
import { registeredChannels, onChannelRegistered } from '@dudousxd/nestjs-diagnostics';
import type { DiagnosticEvent } from '@dudousxd/nestjs-diagnostics';
/** Subscribe a handler to every channel (current + future); returns a teardown. */
export function observeAll(handler: (e: DiagnosticEvent) => void): () => void {
const offs: Array<() => void> = [];
const subscribe = (name: string) => {
const listener = (msg: unknown) => handler(msg as DiagnosticEvent);
diagnostics_channel.subscribe(name, listener);
offs.push(() => diagnostics_channel.unsubscribe(name, listener));
};
for (const name of registeredChannels()) subscribe(name); // every channel so far…
offs.push(onChannelRegistered(subscribe)); // …and any registered later
return () => offs.forEach((off) => off());
}observeAll is the whole pattern: every consumer below is just a handler plugged into it from a NestJS provider, subscribed on boot and torn down on shutdown.
Telescope (batteries-included)
The simplest consumer is Telescope via the generic watcher — no code, every event becomes a diagnostic entry:
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { nestjsDiagnosticsTelescope } from '@dudousxd/nestjs-diagnostics-telescope';
TelescopeModule.forRoot({
extensions: [nestjsDiagnosticsTelescope()],
});Each event lands tagged lib:<lib> / event:<event>, with content = the payload and the envelope's traceId for correlation. When an emitter stamped a durationMs onto the event, the watcher forwards it to the recorder — so the Telescope OTel exporter can feed it into a duration histogram instead of only a counter.
OpenTelemetry
Map each diagnostic event onto the active span as a span event (and attributes). Now your ecosystem's domain events show up inline on your traces. Wire it in a provider so the subscription follows the app lifecycle:
import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
import { trace } from '@opentelemetry/api';
@Injectable()
export class OtelDiagnosticsBridge implements OnModuleInit, OnModuleDestroy {
private stop?: () => void;
onModuleInit() {
this.stop = observeAll((e) => {
trace.getActiveSpan()?.addEvent(`${e.lib}.${e.event}`, {
'aviary.lib': e.lib,
'aviary.event': e.event,
'aviary.trace_id': e.traceId ?? '',
// present only on timed events — feed it into a histogram, not just an attribute
...(e.durationMs !== undefined && { 'aviary.duration_ms': e.durationMs }),
...flatten(e.payload), // your own payload → attribute mapping
});
});
}
onModuleDestroy() {
this.stop?.();
}
}Register OtelDiagnosticsBridge in a module's providers and Nest handles the rest.
Because emit already correlates with traceId (from nestjs-context), you can also stitch events to spans by id when there's no active span on the emitting path — e.g. in a background worker.
An APM or a logger
Forwarding to Datadog, Sentry, an HTTP collector, or just structured logs is the same shape — the same provider, a different handler body:
@Injectable()
export class DiagnosticsLogger implements OnModuleInit, OnModuleDestroy {
private stop?: () => void;
onModuleInit() {
this.stop = observeAll((e) => {
logger.info('aviary.diagnostic', {
lib: e.lib,
event: e.event,
traceId: e.traceId,
ts: e.ts,
...e.payload,
});
// or: apm.captureEvent(...), fetch(collectorUrl, { method: 'POST', body: JSON.stringify(e) }), …
});
}
onModuleDestroy() {
this.stop?.();
}
}Keep consumers cheap and non-blocking. The emit path is guarded (hasSubscribers, never throws), but a subscriber runs inline on the producer's call. Do heavy work (network, disk) off the hot path — batch, queue, or hand off to a worker.
A custom subscriber
You don't need a generic observer at all if you only care about one event. Subscribe to a single channel by name — again from a provider, so you also unsubscribe on shutdown:
import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
import diagnostics_channel from 'node:diagnostics_channel';
import { channelName } from '@dudousxd/nestjs-diagnostics';
import type { DiagnosticEvent } from '@dudousxd/nestjs-diagnostics';
@Injectable()
export class DenialAlerter implements OnModuleInit, OnModuleDestroy {
private readonly channel = channelName('authz', 'decision');
private readonly handler = (msg: unknown) => {
const e = msg as DiagnosticEvent;
if (e.payload.allowed === false) this.alertOnRepeatedDenials(e);
};
onModuleInit() {
diagnostics_channel.subscribe(this.channel, this.handler);
}
onModuleDestroy() {
diagnostics_channel.unsubscribe(this.channel, this.handler);
}
}This is the lowest-overhead way to react to a specific ecosystem event — a denial alert, a metric counter, a webhook.
Across processes (Redis relay)
The diagnostics bus is in-process: a subscriber only sees events emitted in its own Node process. When you run multiple processes — an API pod and a background worker, several replicas — and want a subscriber in one to react to events emitted in another, put the optional @dudousxd/nestjs-diagnostics-redis relay in front of the bus. It forwards selected local aviary:<lib>:<event> channels onto Redis pub/sub and re-emits Redis-received events back onto the local bus, so every subscriber on this page — observeAll, a single-channel handler, Telescope — fires across processes. The diagnostics core stays untouched; this is entirely opt-in.
pnpm add @dudousxd/nestjs-diagnostics-redis @dudousxd/nestjs-diagnostics ioredisSupply a publisher and a separate subscriber connection (a subscribed ioredis connection can't publish, so use redis.duplicate()). Prefer forRootAsync so the relay's clients come from the same DI container as the rest of your app — inject your existing Redis provider and build the options in a factory:
import type Redis from 'ioredis';
import { Module } from '@nestjs/common';
import { DiagnosticsRedisModule } from '@dudousxd/nestjs-diagnostics-redis';
import { REDIS, RedisModule } from './redis.module'; // your app's Redis provider
@Module({
imports: [
DiagnosticsRedisModule.forRootAsync({
imports: [RedisModule], // modules whose providers the factory injects
inject: [REDIS], // tokens passed to useFactory, in order
useFactory: (redis: Redis) => ({
pub: redis,
sub: redis.duplicate(),
libs: ['durable', 'notifications'], // forward all events of these libs
}),
}),
],
})
export class AppModule {}forRootAsync({ imports?, inject?, useFactory }) mirrors the usual Nest convention: imports makes other modules' exported providers available, inject lists the tokens handed to useFactory (in order), and useFactory returns the relay options (pub, sub, and a channel selection — libs, channels, or all). The factory may be async.
Already holding the connections?
The static forRoot({ pub, sub, libs }) takes pre-built clients directly. Reach for forRootAsync whenever your Redis client itself lives in DI — it keeps the relay on the same connection lifecycle as everything else, instead of a top-level new Redis(...).
The relay never opens or closes your Redis connections — you own their lifecycle — and it's loop-safe (a process skips its own echoes, so two pods never ping-pong). Note that payloads cross the boundary as JSON: class instances arrive as plain objects and Date as an ISO string, so cross-process handlers should read fields, not behavior.
In tests
Subscribing makes events assertable. Drop a collector in your test, exercise the code, and check what was emitted:
import diagnostics_channel from 'node:diagnostics_channel';
import { channelName } from '@dudousxd/nestjs-diagnostics';
const seen: any[] = [];
const channel = channelName('authz', 'decision');
const handler = (msg: unknown) => seen.push(msg);
diagnostics_channel.subscribe(channel, handler);
// … exercise the code that emits …
diagnostics_channel.unsubscribe(channel, handler);
expect(seen).toHaveLength(1);
expect(seen[0].payload.allowed).toBe(false);Note that, by design, events are only built when something is subscribed — so asserting on them requires an active subscription, exactly as above.