{ "index": 154, "slug": "editorial-2023-09-field-telemetry-signals", "title": "Ошибка без причины: как связать метрику, trace и log", "excerpt": "График показывает класс ошибки, но не объясняет один запрос. Разбираем маршрут metric → trace → log/event, границу cardinality и проверку, которая не выдаёт учебный пример за production-доказательство.", "contentHtml": "

График ошибок растёт, но инженер не может назвать запрос и этап, на котором возник отказ. В журналах много похожих сообщений, а в trace-поиске нет понятного ключа. Самая дорогая ошибка в этот момент — принять громкий сигнал за причину: увеличить timeout, добавить retry или обвинить downstream. Сбой может остаться, а новые записи и задержки вырастут.

\n

Проблема возникает, когда metric, trace и log описывают один путь разными словами. Метрика считает класс исходов. Trace показывает путь запроса через операции. Log или event фиксирует событие и его контекст. Если между ними нет общего договора, команда видит три витрины, а не одну проверяемую цепочку.

\n

Тезис: каждый сигнал отвечает на свой вопрос

\n

Начинайте с вопроса, а не с поиска текста ошибки. Metric отвечает: «какой класс исходов изменился?». Trace отвечает: «через какие операции прошёл один путь?». Log/event отвечает: «какое событие произошло на конкретном шаге?». Общий trace ID или другой разрешённый correlation key связывает записи. Он не превращает metric в журнал запросов.

\n

Идентификатор одного запроса нельзя бездумно добавлять в labels метрики. Каждый новый идентификатор может создавать отдельный time series. График станет дороже, агрегация — менее полезной, а проблема поиска не исчезнет. Для метрики оставляют небольшой словарь: service, route и outcome. Подробный контекст отправляют в trace или log после проверки политики доступа и хранения.

\n

Механизм маршрута

\n

Представим учебный checkout-сценарий. Metric сообщает: для маршрута authorization вырос класс rejected. Эта запись не знает пользователя, заказа и конкретного trace. Она только выбирает поле поиска. Далее trace с тем же synthetic correlation key показывает gateway span и дочерний payment span. Затем log/event указывает, что отказ произошёл на payment span, и повторяет trace ID и span ID.

\n

Каждая стрелка требует отдельной проверки. Наличие метрики не доказывает существование trace. Наличие trace не доказывает, что log экспортирован и доступен. Совпавший ID не доказывает причину отказа, если событие записалось после ошибки или относится к другому шагу. Доказательство должно состоять из наблюдаемых объектов и честного статуса каждой связи.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Счётчик ошибок изменился, trace не находитсяНет перехода от route/outcome к trace или trace не экспортируетсяВзять один разрешённый outcome и проверить correlation в реальной средеПочинить передачу контекста или назвать путь неподтверждённым
В metric появились request IDИдентичность запроса использовали как labelПосчитать набор labels и рост series на выбранном окнеОстановить изменение, вернуть малый словарь labels, ID оставить в trace/log
Trace есть, событие не объясняет отказLog не содержит span ID, событие относится к другому шагу или потеряно при samplingСверить trace ID, span ID, имя события и времяИсправить корреляцию; не объявлять downstream причиной
Свободный текст не ищется стабильноСообщение меняется между версиями и не имеет event nameПроверить структурированные поля и стабильное имя событияДобавить минимальную схему и сохранить текст как дополнительный context
Учебный тест зелёный, production неизвестенПроверили форму записей в памяти, а не экспорт и поискОтделить fixture от реальной выборки и явно отметить границуНазначить проверку в разрешённой среде; не публиковать результат как incident evidence
\n

Конкретный пример

\n

Ниже — классификатор учебных записей. Он проверяет только договор между объектами в памяти. Значения synthetic-* выдуманы для примера. Код не обращается к приложению, не создаёт telemetry и не подтверждает, что downstream действительно вернул ошибку.

\n
function checkRoute({ metric, trace, event }) {\n  const labels = Object.keys(metric.labels);\n  const allowed = ['service', 'route', 'outcome'];\n  const metricShape = labels.length === 3\n    && labels.every((name) => allowed.includes(name))\n    && !labels.includes('trace_id');\n  const traceShape = trace.root.traceId === trace.payment.traceId;\n  const eventShape = event.traceId === trace.payment.traceId\n    && event.spanId === trace.payment.spanId;\n  return {\n    metricShape,\n    traceShape,\n    eventShape,\n    readyForRealCheck: metricShape && traceShape && eventShape,\n  };\n}\n\nconst result = checkRoute({\n  metric: { labels: { service: 'checkout', route: 'authorization', outcome: 'rejected' } },\n  trace: {\n    root: { traceId: 'synthetic-trace-1' },\n    payment: { traceId: 'synthetic-trace-1', spanId: 'synthetic-span-payment' },\n  },\n  event: { traceId: 'synthetic-trace-1', spanId: 'synthetic-span-payment' },\n});\n\nconsole.log(result);\n// readyForRealCheck: true — только договор synthetic-записей.
\n

Отрицательный путь важнее зелёного результата. Если event получит другой trace ID, eventShape станет false. Если в metric появится trace_id, metricShape станет false. Код не угадывает причину и не исправляет систему. Он останавливает вывод: сначала нужно восстановить связь или признать, что её нет.

\n
\"Учебный
Учебная схема разделяет вопросы сигналов. Она не изображает реальный alert, запрос к backend, trace search или подтверждённую production-причину.
\n

Как читать три сигнала вместе

\n

Metric полезна на первом шаге, потому что сжимает поток в устойчивые классы. Используйте route template и outcome, а не полный URL, user ID, order ID или текст ошибки. Набор dimensions должен быть заранее ограничен. Точное число допустимых series зависит от платформы, окна и числа значений, поэтому его нельзя объявлять безопасным без расчёта и проверки владельца backend.

\n

Trace нужен, когда вопрос перешёл от класса к пути. Найдите один разрешённый trace и проверьте дерево spans: gateway должен вести к payment operation, а не просто соседствовать с ней по времени. Сверьте parent-child связь, статус, длительность и границы sampling. Даже полный trace показывает путь инструментирования, а не автоматически истинную причину бизнес-ошибки.

\n

Log/event нужен для контекста шага. Структурированное событие должно иметь стабильное имя, время, trace ID и, если событие связано с конкретной операцией, span ID. Дополнительные attributes должны пройти review на чувствительные данные, redaction, retention и права доступа. «Добавим весь request на всякий случай» — плохая стратегия: она увеличивает риск и не делает гипотезу точнее.

\n

Порядок действий

\n
  1. Опишите симптом одним предложением: какой класс исходов изменился, в каком route и за какое окно.
  2. Назовите ожидаемый переход metric → trace. Проверьте, что metric не содержит per-request labels и использует малый словарь service, route, outcome.
  3. Выберите один разрешённый trace. Сверьте trace ID, root span, дочерний span и время операции. Не делайте вывод по одному графику.
  4. Найдите log/event на конкретном span. Проверьте event name, trace ID, span ID и отсутствие лишних чувствительных полей.
  5. Прогоните отрицательные проверки: mismatch trace ID, mismatch span ID, лишний label и отсутствие event. Каждый случай должен останавливать вывод.
  6. Сформулируйте действие только после проверки связи. Если trace или event отсутствует, исправляйте instrumentation и экспорт, а не таймаут downstream.
  7. Повторите проверку тем же route, окном и правилом выборки. Сравните стоимость series, доступность поиска и соседние сигналы.
  8. Запишите результат как подтверждённый, неподтверждённый или неполный. Не называйте synthetic PASS наблюдением production.
\n

Когда остановиться и что откатывать

\n

Если новый label резко расширяет cardinality или event содержит запрещённое поле, остановите распространение изменения. Сначала определите, какие записи ещё могут появляться и какие потребители уже зависят от схемы. Затем выберите обратимое действие для конкретной конфигурации: отключить добавленный label, ограничить event attributes или вернуть предыдущую версию instrumentation. Нельзя обещать удаление уже сохранённых данных, пока не известны storage, retention и политика доступа.

\n

Если metric уже есть, а trace не связывается, не добавляйте ещё один ID в счётчик. Проверьте propagation на границе сервиса, sampling, exporter и возможность поиска. Если log не содержит span ID, назовите это дефектом корреляции. Если настоящая система не позволяет безопасно проверить путь, остановите расследование на статусе «не подтверждено» и не заменяйте evidence догадкой.

\n

Ограничения и критерий готовности

\n

Пример не содержит реальных logs, metrics, traces, latency, traffic, backend records или incident data. Synthetic value и IDs не являются измерениями. Статья не утверждает, что конкретная SDK, collector, exporter или backend поддерживает одинаковые поля и поиск. Sampling может скрыть часть trace. Асинхронная очередь может разорвать контекст. Событие может прийти позже операции. Эти условия нужно проверять в выбранном контуре.

\n

Критерий готовности проверяемый: для одного разрешённого route есть metric с заранее названными dimensions; для выбранного outcome найден trace с тем же correlation key; trace содержит ожидаемый span; log/event имеет тот же trace ID и корректный span ID; отрицательные ветки дают отказ; после изменения не выросли запрещённые labels и не появились чувствительные поля. Если хотя бы одна связь не доказана, итог — неполный.

\n

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

" }