Node.js diagnostics_channel: встроенная трассировка без внешних библиотек
В Node.js есть встроенный механизм трассировки, о котором мало кто знает — diagnostics_channel. Он появился в Node 16 и позволяет подписываться на внутренние события рантайма без monkey-patching и внешних APM-библиотек. Разбираем API, встроенные каналы и интеграцию с OpenTelemetry.
Что такое diagnostics_channel
Модуль diagnostics_channel — pub/sub-система для диагностических данных. Библиотека публикует события в именованный канал, потребитель (APM, логгер) подписывается без изменения исходного кода.
const dc = require('diagnostics_channel');
const channel = dc.channel('my-app:request');
// Подписка
channel.subscribe((message, name) => {
console.log(`[${name}]`, message);
});
// Публикация
channel.publish({ url: '/api/users', method: 'GET' });
Отличия от EventEmitter:
- Глобальный реестр — канал доступен по имени из любого модуля.
- Zero-cost без подписчиков —
publish()практически бесплатен, если никто не слушает. - Простой API — только publish/subscribe, без once/removeListener.
С Node 19+ доступен tracingChannel() для обёртки async-операций:
const { tracingChannel } = require('diagnostics_channel');
const tracing = tracingChannel('my-app:db-query');
// Автоматически публикует start, end, asyncStart, asyncEnd, error
const result = await tracing.tracePromise(asyncFn, context);
Встроенные каналы: что отслеживать из коробки
Node.js публикует события для ключевых подсистем:
const dc = require('diagnostics_channel');
// HTTP-клиент — исходящие запросы
dc.subscribe('http.client.request.start', ({ request }) => {
console.log(`→ ${request.method} ${request.host}${request.path}`);
});
// HTTP-сервер — входящие запросы
dc.subscribe('http.server.request.start', ({ request }) => {
request.__startTime = Date.now();
});
dc.subscribe('http.server.response.finish', ({ request, response }) => {
const ms = Date.now() - request.__startTime;
console.log(`${request.method} ${request.url} ${response.statusCode} ${ms}ms`);
});
// Net — TCP-соединения
dc.subscribe('net.client.socket', ({ socket }) => {
console.log(`TCP → ${socket.remoteAddress}:${socket.remotePort}`);
});
В Node 22+ добавлены каналы для undici (fetch), dns, module (ESM loading).
Создание своих каналов
Инструментируйте бизнес-логику без привязки к конкретному APM:
// lib/payments.js
const dc = require('diagnostics_channel');
const paymentStart = dc.channel('app:payment.start');
const paymentEnd = dc.channel('app:payment.end');
async function processPayment(order) {
const ctx = { orderId: order.id, amount: order.total, startTime: Date.now() };
paymentStart.publish(ctx);
try {
const result = await gateway.charge(order);
paymentEnd.publish({ ...ctx, duration: Date.now() - ctx.startTime });
return result;
} catch (err) {
dc.channel('app:payment.error').publish({ ...ctx, error: err.message });
throw err;
}
}
// instrumentation.js — подключается через --require
dc.subscribe('app:payment.end', (msg) => {
metrics.histogram('payment.duration', msg.duration);
});
Запуск: node --require ./instrumentation.js app.js — инструментация без изменения кода приложения.
Интеграция с OpenTelemetry
OpenTelemetry использует diagnostics_channel как источник данных вместо monkey-patching:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const provider = new NodeTracerProvider();
provider.register();
// HttpInstrumentation внутри подписывается на diagnostics_channel
Для своих каналов создавайте спаны вручную:
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('my-app');
dc.subscribe('app:payment.start', (msg) => {
msg.__span = tracer.startSpan('payment.process', {
attributes: { 'order.id': msg.orderId }
});
});
dc.subscribe('app:payment.end', (msg) => msg.__span?.end());
Производительность
Замеры overhead на 1M вызовов (Node 22):
Механизм | С подписчиком | Без подписчика
────────────────────────────────────────────────────────────
diagnostics_channel | 48 ns/op | 3 ns/op
EventEmitter | 52 ns/op | 35 ns/op
AsyncLocalStorage.run() | 210 ns/op | —
Без подписчиков — 3 наносекунды (проверка hasSubscribers). EventEmitter тратит 35 ns даже без слушателей. Для горячих путей оборачивайте:
if (channel.hasSubscribers) {
channel.publish({ ... });
}
diagnostics_channel — правильный способ добавить observability в Node.js: стандартный API, минимальный overhead, совместимость с любым APM через подписку.