Node.js, part 15: diagnostics_channel — publishing what happens inside your app, without an event emitter
Part 15from the Node.js series · 24 parts in all
Instrumentation has an awkward requirement: the code that reports must not depend on the
code that listens, and neither may depend on the other's absence. An EventEmitter
fails that — emitting to nobody is a no-op only by luck, and 'error' without a
listener is a crash. diagnostics_channel is a one-way, zero-or-many, no-dependency
notification bus.
Publish and subscribe, decoupled
const dc = require('diagnostics_channel');
// Library side: announce, but never require anyone to be listening.
const channel = dc.channel('maplecart.order.created');
if (channel.hasSubscribers) {
channel.publish({ orderId, total, at: Date.now(), traceId });
}
// App side: subscribe wherever the metrics live.
dc.subscribe('maplecart.order.created', (msg) => {
metrics.histogram('order_total', msg.total);
});
The hasSubscribers guard is the point of the API: when nobody has subscribed,
the object literal is never built. So instrumentation can stay in a hot path without costing
anything in the case that matters most — production, where you have not turned it on.
Why not just an emitter
- No coupling. The library needs no reference to the metrics object, no injection, no global. It publishes to a string.
- Synchronous and ordered. Publish calls subscribers immediately, so the data is captured at the moment of the event rather than scheduled behind other work.
- It is how Node instruments itself. Core modules publish on channels
(
http.server.request,net.server.connection, and more since), which means an APM agent can observe the runtime without monkey-patchinghttp.createServer— a technique that used to break on every minor release.
The pattern to copy
// Cheap, always-on counters next to the expensive, opt-in payload.
function checkOrder(order) {
const ch = dc.channel('maplecart.order.check');
if (!ch.hasSubscribers) return;
ch.publish({ orderId: order.id, rules: order.matchedRules });
}
Pair it with the tracing context from part 10 and you have the shape most Node APM agents
are built on: an AsyncLocalStorage holding the trace, and channels publishing the
events. Next: Node 18, the release that made a lot of dependencies unnecessary.