Trust is earned, not given

A different perspective

2021-09-28 · Projects

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

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.