{ "index": 156, "slug": "editorial-2023-09-practice-telemetry-signals", "title": "Логи, метрики и трассы: как связать один сбой без взрыва cardinality", "excerpt": "Пошаговая схема корреляции для распределённого запроса: метрика показывает класс отказа, trace — путь, а структурированный лог — причину конкретного события.", "contentHtml": "

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

\n

Рабочая схема разделяет три роли. Метрика отвечает, как часто возникает класс событий. Trace, то есть распределённая трасса, показывает путь одного запроса через сервисы. Структурированный лог фиксирует событие и его безопасный контекст. Один trace ID связывает trace и лог, но не должен становиться label метрики: иначе корреляция создаст неконтролируемое число временных рядов.

\n

Начните с вопроса расследования

\n

До настройки SDK и дашборда запишите, какой факт требуется получить. Для всплеска ошибок авторизации вопрос звучит так: «какие операции и на каких границах отклоняют запросы?». У каждого сигнала будет свой ответ, поэтому один идентификатор нельзя механически разложить по всем полям.

\n
СигналВопросПример поляОграничение
МетрикаКак меняется частота класса?outcome=rejectedНе указывает запрос
TraceКакие шаги прошёл запрос?trace_id, span_idНе сохраняет каждый запрос
ЛогЧто произошло на шаге?event_name, reason_classНе заменяет агрегат
\n

OpenTelemetry описывает trace как путь запроса, metric как измерение во время работы, а log как запись события. Сигнал выбирают по вопросу, а не по открытому у инженера хранилищу.

\n

Опишите контракт на границах сервисов

\n

Рассмотрим запрос checkout. Gateway принимает HTTP-запрос, передаёт контекст сервису оплаты, а payment создаёт дочерний span authorize. При отказе payment пишет событие и увеличивает счётчик. Контракт проверяется по пяти переходам:

\n
  1. На входе gateway прочитать или создать trace context.
  2. Передать контекст на исходящем вызове в payment.
  3. Создать дочерний span вокруг авторизации.
  4. Записать из активного контекста тот же trace ID и span ID фактической причины.
  5. Увеличить метрику с labels сервиса, нормализованного маршрута и класса результата.
\n

Для HTTP таким переносом обычно служит заголовок traceparent, определённый W3C Trace Context. Он не является пользовательским request ID и должен проходить проверку формата. На каждой границе сравнивайте trace ID: downstream продолжает тот же trace, а не начинает новый. Если клиент не поддерживает propagation, исправляйте интеграцию, а не добавляйте trace ID в metric.

\n
\"Учебная
Trace и лог связываются по контексту запроса; метрика сохраняет только класс события и остаётся агрегируемой.
\n

Оставьте уникальные значения вне labels

\n

В модели Prometheus каждый уникальный набор значений labels создаёт отдельный временной ряд. Если добавить к счётчику trace_id, почти каждый запрос создаст новый ряд. Тот же риск несут user_id, order_id, email и сырой URL с идентификаторами. Backend может принять такие значения, но стоимость хранения и запросов растёт вместе с комбинациями.

\n

В label оставляйте поля с ограниченным словарём. Вместо /orders/8472 используйте /orders/:id; вместо текста исключения — класс limit, invalid_input или upstream_timeout. Список классов — часть контракта и должен быть согласован с владельцем дашборда.

\n
ПолеTrace или logMetric labelПричина
trace_idДа, для перехода к путиНетПочти неограниченное множество
route_templateДаДаОграниченный словарь
reason_classДаДа, если согласованГруппирует причины
order_idТолько при разрешённом доступеНетВысокая cardinality и чувствительность
\n

Проверьте договор на учебных данных

\n

Это форма договора, а не готовый OpenTelemetry exporter. Идентификаторы с префиксом demo- вымышлены. Фрагмент проверяет совпадение trace ID в trace и логе и отсутствие уникального поля в metric sample.

\n
const trace = {\n  traceId: 'demo-trace-001',\n  spans: [\n    { spanId: 'demo-gateway', service: 'gateway', operation: 'checkout' },\n    { spanId: 'demo-payment', service: 'payment', operation: 'authorize' },\n  ],\n};\nconst logEvent = {\n  trace_id: trace.traceId,\n  span_id: 'demo-payment',\n  event_name: 'payment.authorization.rejected',\n  reason_class: 'limit',\n};\nconst metricSample = {\n  name: 'payment_authorization_total',\n  labels: { service: 'payment', route: 'checkout', outcome: 'rejected' },\n  value: 1,\n};\nconsole.assert(logEvent.trace_id === trace.traceId);\nconsole.assert(!Object.hasOwn(metricSample.labels, 'trace_id'));
\n

Проверки подтверждают только форму объекта. В рабочем сервисе данные должны быть результатом инструментирования и экспортироваться в выбранный backend. Demo-данные не показывают реальную задержку, частоту отказов или полноту sampling.

\n

Воспроизведите связь через HTTP

\n

Если тестовый gateway слушает localhost:8080, передайте ему фиксированный учебный контекст. Заголовок соответствует формату W3C и предназначен для лабораторной проверки. Замените URL и имя файла на настройки своей среды.

\n
curl -sS -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' http://localhost:8080/checkout\njq 'select(.trace_id == \"4bf92f3577b34da6a3ce929d0e0e4736\") | {trace_id, span_id, event_name}' app.log
\n

Сверьте три наблюдения: в trace появился путь gateway → payment; в логе есть тот же trace_id и span оплаты; график показывает серию service=payment, route=checkout, outcome=rejected. HTTP 200 этого не доказывает. Если endpoint не создаёт отказ, используйте тестовый сценарий с известным ответом.

\n

Разберите симптом по таблице

\n
НаблюдениеГипотезаПроверкаДействие
График растёт, trace не находитсяНет перехода к контексту или trace отброшен samplingВзять лог отказа и найти его trace IDНастроить переход; sampling проверить отдельно
Новая series появляется почти на каждый запросВ label попал ID или сырой URLПосчитать значения label за окноУдалить уникальное поле и нормализовать маршрут
Trace общий, span указывает gatewayЛог пишется вне активного spanСопоставить span ID с операцией отказаПисать событие внутри нужного контекста
Payment видит новый traceНе сработал propagatorСравнить входящий и исходящий traceparentИсправить middleware или клиент
\n

Таблица отделяет неисправность контекста от sampling и плохой схемы labels. После исправления повторите тот же тестовый запрос.

\n

Проверьте отрицательные сценарии

\n

Счастливый путь доказывает лишь то, что корреляция иногда работает. Подмените span ID в логе и убедитесь, что проверка указывает на неверный шаг. Уберите trace context и проверьте, что запись без trace ID не смешивается с другой трассой. Запустите два параллельных запроса: одинаковая временная метка не может быть единственным ключом связи.

\n

Отдельно проверьте рост словаря labels. В тестовом инструменте добавьте уникальный ID намеренно, посчитайте новые серии, затем удалите его и сравните число рядов с исходным диапазоном. Для этого достаточно изолированного Prometheus-compatible backend; production-трафик не нужен.

\n

Зафиксируйте границы применимости

\n

Схема не гарантирует trace для каждого отказа. Sampling может сохранить только часть запросов, сборщик — потерять данные, а политика хранения — удалить старые записи. Метрика показывает агрегированный класс, но не полный список причин. Для критичных операций заранее определите sampling и срок хранения.

\n

Trace ID не является разрешением на доступ к данным. Ссылка из метрики в trace должна учитывать права пользователя. Логи с trace ID всё равно могут содержать персональные данные, токены или платёжные реквизиты; корреляция не отменяет маскирование и ограничение доступа.

\n

Низкая cardinality не означает низкую стоимость для любого backend. Prometheus описывает labels как измерения временных рядов и предупреждает о high-cardinality values. Для другой системы уточните модель хранения, индексацию и sampling. Имена полей зависят от языка и SDK.

\n

Критерий готовности

\n

Договор проверен, когда тестовый запрос проходит нужные границы, downstream сохраняет общий trace ID, лог содержит trace ID и span ID фактической причины, а метрика группируется по ограниченным labels. Зафиксируйте также тест потери контекста, проверку cardinality и правило доступа к логам и трассам. Иначе dashboard показывает сигнал, но не даёт воспроизводимого маршрута расследования.

\n

Проверяемые источники

\n" }