lenec ru

← все посты

Node.js diagnostics_channel: встроенная трассировка без внешних библиотек

10K

В 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 через подписку.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.