{ "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Начинайте с вопроса, а не с поиска текста ошибки. Metric отвечает: «какой класс исходов изменился?». Trace отвечает: «через какие операции прошёл один путь?». Log/event отвечает: «какое событие произошло на конкретном шаге?». Общий trace ID или другой разрешённый correlation key связывает записи. Он не превращает metric в журнал запросов.
\nИдентификатор одного запроса нельзя бездумно добавлять в labels метрики. Каждый новый идентификатор может создавать отдельный time series. График станет дороже, агрегация — менее полезной, а проблема поиска не исчезнет. Для метрики оставляют небольшой словарь: service, route и outcome. Подробный контекст отправляют в trace или log после проверки политики доступа и хранения.
\nПредставим учебный checkout-сценарий. Metric сообщает: для маршрута authorization вырос класс rejected. Эта запись не знает пользователя, заказа и конкретного trace. Она только выбирает поле поиска. Далее trace с тем же synthetic correlation key показывает gateway span и дочерний payment span. Затем log/event указывает, что отказ произошёл на payment span, и повторяет trace ID и span ID.
Каждая стрелка требует отдельной проверки. Наличие метрики не доказывает существование 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 |
Ниже — классификатор учебных записей. Он проверяет только договор между объектами в памяти. Значения synthetic-* выдуманы для примера. Код не обращается к приложению, не создаёт telemetry и не подтверждает, что downstream действительно вернул ошибку.
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. Код не угадывает причину и не исправляет систему. Он останавливает вывод: сначала нужно восстановить связь или признать, что её нет.
Metric полезна на первом шаге, потому что сжимает поток в устойчивые классы. Используйте route template и outcome, а не полный URL, user ID, order ID или текст ошибки. Набор dimensions должен быть заранее ограничен. Точное число допустимых series зависит от платформы, окна и числа значений, поэтому его нельзя объявлять безопасным без расчёта и проверки владельца backend.
\nTrace нужен, когда вопрос перешёл от класса к пути. Найдите один разрешённый trace и проверьте дерево spans: gateway должен вести к payment operation, а не просто соседствовать с ней по времени. Сверьте parent-child связь, статус, длительность и границы sampling. Даже полный trace показывает путь инструментирования, а не автоматически истинную причину бизнес-ошибки.
\nLog/event нужен для контекста шага. Структурированное событие должно иметь стабильное имя, время, trace ID и, если событие связано с конкретной операцией, span ID. Дополнительные attributes должны пройти review на чувствительные данные, redaction, retention и права доступа. «Добавим весь request на всякий случай» — плохая стратегия: она увеличивает риск и не делает гипотезу точнее.
\nЕсли новый label резко расширяет cardinality или event содержит запрещённое поле, остановите распространение изменения. Сначала определите, какие записи ещё могут появляться и какие потребители уже зависят от схемы. Затем выберите обратимое действие для конкретной конфигурации: отключить добавленный label, ограничить event attributes или вернуть предыдущую версию instrumentation. Нельзя обещать удаление уже сохранённых данных, пока не известны storage, retention и политика доступа.
\nЕсли metric уже есть, а trace не связывается, не добавляйте ещё один ID в счётчик. Проверьте propagation на границе сервиса, sampling, exporter и возможность поиска. Если log не содержит span ID, назовите это дефектом корреляции. Если настоящая система не позволяет безопасно проверить путь, остановите расследование на статусе «не подтверждено» и не заменяйте evidence догадкой.
\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