← назад к разделу

Алерт сообщает: p99 оформления заказа вырос до четырех секунд. Дежурный открывает график, поиск трасс и поиск логов. Через полчаса выясняется, что запросы к базе в логах есть, но непонятно, к какому HTTP-запросу они относятся. Проблема не в инструментах, а в отсутствии связей между ними.

Три сигнала отвечают на разные вопросы: метрика - сколько и насколько плохо, трассировка - где именно, лог - что произошло. Сквозной trace_id превращает их в одно расследование.

Связь начинается до алерта

OpenTelemetry создает trace и spans, @opentelemetry/instrumentation-pino добавляет идентификаторы активного span в записи Pino. Инициализация трассировки должна идти до загрузки NestJS и инструментируемых библиотек.

import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';

const sdk = new NodeSDK({
  instrumentations: [
    getNodeAutoInstrumentations(),
    new PinoInstrumentation(),
  ],
});

sdk.start();

В логе должны быть одинаково названные поля trace_id и span_id, а не traceId в одном сервисе и requestId вместо них в другом. requestId полезен дополнительно, но не заменяет идентификатор трассы.

Проверка простая: взять trace_id из любой записи production и найти по нему полный trace. Если его нет, сначала чинят контекст, а не строят новый дашборд.

Шаг 1: измерить масштаб, а не искать виноватого сразу

Сначала отвечают на вопросы:

  • один route или все;
  • одна версия после выката или обе;
  • один Pod или все реплики;
  • ошибки, задержка или исчерпание очереди;
  • рост event-loop lag, ожидание PostgreSQL или внешнего HTTP.

Labels остаются ограниченными: service, version, route, method, status_class. orderId и реальный URL в метрики не добавляют. P99 и гистограмма показывают хвост, среднее скрывает его.

Шаг 2: выбрать конкретный запрос

Если используемый клиент метрик и формат OpenMetrics поддерживают exemplars, гистограмма может содержать ссылку на sampled trace рядом с наблюдением. Это не label и не рост cardinality: exemplar прикладывается к отдельному значению. Поддержку проверяют на фактической версии библиотеки Node.js, а не предполагают по аналогии с Go или Java.

Без exemplars выбирают медленные или ошибочные traces за тот же короткий интервал и с теми же service, route и version. Дальше важен не любой медленный запрос, а представительный: тот же сценарий и похожая длительность.

Шаг 3: найти место в трассе

В трассе смотрят порядок и длительность spans:

  • один внешний HTTP занимает 3,7 секунды из 4;
  • двести PostgreSQL spans по 10 миллисекунд означают N+1;
  • длинный синхронный участок без дочерних spans требует CPU-профиля;
  • ожидание pool.acquire отличается от выполнения SQL;
  • AbortError не доказывает, что партнер не выполнил операцию.

Если трассировка обрывается на очереди, следующий сервис выглядит невиновным. Контекст надо передавать и там.

Шаг 4: собрать логи по trace_id

Теперь поиск идет не «все ошибки за полчаса», а trace_id = ... по всем сервисам. В логах должны быть orderId, paymentId и другие безопасные бизнес-ключи. Они связывают асинхронные шаги и остаются полезны даже при потере trace context.

Если идентификатор потерян, порядок такой:

  1. бизнес-ключ, например orderId;
  2. сервис и узкое окно времени из родительского span;
  3. уровень WARN и выше, затем все записи по найденному requestId.

Не добавляйте email, authorization headers, токены и полные платежные данные ради диагностики.

Очередь и фоновая работа

AsyncLocalStorage не переносится через BullMQ, Kafka или отдельный процесс автоматически. При постановке работы traceparent и requestId сериализуют в payload, в обработчике восстанавливают OTel context и лог-контекст.

const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);

await queue.add('send-receipt', {
  orderId,
  traceparent: carrier.traceparent,
  tracestate: carrier.tracestate,
});

В обработчике propagation.extract и context.with связывают новый span с исходной трассой. Простой setTimeout внутри того же async-дерева контекст обычно сохраняет, а process.nextTick, очередь и новый worker - разные случаи, которые проверяют отдельно.

Когда трассы нет

Остается дисциплина логов:

  • один access log с requestId, route, status и duration;
  • одна запись на исходящий вызов с system, operation, duration и результатом;
  • те же поля на чтении и записи сообщения;
  • безопасный бизнес-ключ в каждом значимом событии.

По этим записям восстанавливают последовательность вручную. Когда появляются несколько сервисов, параллельные вызовы и N+1, трассировка становится дешевле ручной сборки.

Что находится в алерте и runbook

Алерт называет симптом для клиента: «p99 оформления выше двух секунд пять минут», а не только PromQL. Runbook содержит:

  • ссылку на дашборд с route и version;
  • запрос медленных traces за окно;
  • поиск логов по trace_id;
  • метрики Node.js: event-loop lag, RSS, heap, active handles;
  • метрики PostgreSQL pool и pg_stat_activity;
  • безопасные действия: вывести реплику из трафика, откатить, включить профиль на время.

Если runbook заканчивается фразой «разобраться», алерт пока не готов.

Почему нужен tail sampling

Head-based sampling выбирает судьбу трассы до того, как известно, закончится ли она ошибкой. Один нужный медленный запрос легко потерять. Tail sampling на коллекторе сохраняет ошибочные и медленные traces и небольшую долю остальных.

Это увеличивает нагрузку на коллектор и требует передачи spans всей цепочки, поэтому параметры измеряют. Для алертов также согласуют сроки хранения traces и logs: если traces живут день, а разбор идет через неделю, связь не поможет.

Чем измерить улучшение

В разбор инцидента добавляют три отметки: начало проблемы, обнаружение и момент понимания причины. Время до диагноза показывает пользу связей лучше, чем общее MTTR: восстановление может быть быстрым откатом, но следующая такая авария все равно потребует часа.

Смотрят медиану и выбросы за квартал по своей команде, без рейтинга людей.

Коротко

  • Общий trace_id связывает метрику, трассу и логи, Pino instrumentation подключается до загрузки приложения.
  • Сначала масштаб по ограниченным labels, затем конкретный trace, место в spans и записи по trace_id.
  • Exemplars полезны там, где фактическая Node.js-библиотека их поддерживает; без них ищут traces за то же окно.
  • Бизнес-ключи помогают при потере контекста, очередь требует явной сериализации traceparent.
  • Алерт содержит симптом и runbook, tail sampling сохраняет важные traces.
  • Время до диагноза измеряют отдельно от времени восстановления.

Что почитать дальше