Diagnostics Channel

Instrumentation with Diagnostics Channel

node:diagnostics_channel is a named publish-and-subscribe bus with one property that makes it usable on hot paths: publishing to a channel nobody listens to costs a property read. Libraries use it to expose timings, query text and retry counts without committing to a public API.

A custom channel and a traced asynchronous operationJavaScript
import dc from 'node:diagnostics_channel';
const cache = dc.channel('app:cache');
cache.subscribe(({ key, hit }) => console.log('cache   ', key, hit ? 'HIT' : 'MISS'));
if (cache.hasSubscribers) cache.publish({ key: 'user:7', hit: false });
const db = dc.tracingChannel('app:db');
db.subscribe({
  start: ({ sql }) => console.log('start   ', sql),
  asyncEnd: ({ result }) => console.log('asyncEnd', result ? `${result.rows} rows` : 'rejected'),
  error: ({ error }) => console.log('error   ', error.message),
});
const query = (sql) => db.tracePromise(async () => {
  if (sql.startsWith('drop')) throw new Error('permission denied');
  return { rows: 3 };
}, { sql });
await query('select * from users');
await query('drop table users').catch(() => {});
Output
cache    user:7 MISS
start    select * from users
asyncEnd 3 rows
start    drop table users
error    permission denied
asyncEnd rejected

A TracingChannel bundles five channels under one name — start, end, asyncStart, asyncEnd and error — and tracePromise(), traceCallback() and traceSync() publish around a call. Every subscriber sees the same context object, so one can stamp a start time in start and read it back in asyncEnd, which fires for a rejection too.

Node publishes to built-in channels as well: 'http.server.request.start', 'net.client.socket' and the 'tracing:module.require:start' family let you watch the runtime itself, and dc.subscribe(name, fn) attaches by name without creating the channel first. The module and tracingChannel() are stable; every built-in channel is Stability 1 - Experimental, so pin the Node version when you build on one.