Алерт сообщает: 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.
Если идентификатор потерян, порядок такой:
- бизнес-ключ, например
orderId; - сервис и узкое окно времени из родительского span;
- уровень 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.
- Время до диагноза измеряют отдельно от времени восстановления.
Что почитать дальше
- Проброс контекста на Node.js - AsyncLocalStorage и очереди.
- Трассировка на Node.js - OpenTelemetry и spans.
- Профилирование и утечки на Node.js - куда идти при росте RSS и event-loop lag.
- SLO и алерты на Node.js - как выбрать сигнал для пробуждения.