{ "index": 155, "slug": "editorial-2023-09-mechanism-telemetry-signals", "title": "Почему trace ID не должен становиться label метрики", "excerpt": "Trace, metric и log отвечают на разные вопросы. Разбираем, как сохранить корреляцию одного запроса, не превратить метрику в журнал событий и проверить отрицательный путь.", "contentHtml": "
Симптом знаком: график отказов растёт, но инженер не может назвать конкретный запрос и этап, на котором он сломался. В логах есть похожие сообщения, а трасса либо не находится, либо не связана с ними. Первый быстрый ремонт — добавить trace_id или request_id в labels метрики. График становится фильтруемым, но перестаёт быть хорошим графиком. Цена ошибки — не абстрактная «плохая наблюдаемость». Команда смешивает счётчик с идентичностью одного запроса, раздувает число временных рядов и принимает решение по данным, которые не отвечают на вопрос о причине.
Тезис простой: общий идентификатор нужен для корреляции trace и log/event record, а labels метрики должны описывать небольшой набор групп, по которым допустима агрегация. Один и тот же атрибут может встретиться в нескольких сигналах, но его роль не становится одинаковой. Сначала определите вопрос сигнала. Потом выбирайте поле.
\nTrace описывает путь операции. Его узлы — spans: например, gateway, вызов каталога и шаг оплаты. У trace есть TraceId, а у каждого span — собственный SpanId. Так можно связать дочернюю операцию с родительской и пройти от общего маршрута к конкретному шагу.
Metric описывает измеряемый класс поведения во времени. Для неё важны имя инструмента, значение, единица, временной ряд и attributes, которые разделяют поток на dimensions. Вопрос метрики звучит как «сколько отказов было на этом маршруте?» или «какое распределение задержки видит этот класс операций?». Вопрос «какой именно request упал?» относится к другой записи.
\nLog или event record фиксирует конкретное событие и его контекст. В него можно положить имя события, класс ошибки, span ID и trace ID, если это разрешено политикой хранения. Такая запись помогает объяснить один отказ. Она не заменяет агрегированную метрику, потому что свободный текст и уникальные идентификаторы плохо отвечают на вопрос о тренде.
\n| Сигнал | Основной вопрос | Подходящие данные | Чего не следует требовать |
|---|---|---|---|
| Trace | Какой путь прошла операция? | trace_id, span_id, родительский span, имя операции | Считать все ошибки и строить долгий тренд |
| Metric | Как меняется класс результата? | route template, service, outcome, environment | Хранить идентичность каждого запроса |
| Log/event | Что произошло на одном шаге? | event name, trace ID, span ID, проверенные attributes | Становиться единственным источником агрегации |
Представим учебный маршрут checkout. Gateway принимает запрос и создаёт корневой span. Дочерний span вызывает оплату. Оплата отклоняет авторизацию. Во всех трёх шагах используется один учебный trace ID, но gateway и payment имеют разные span ID. Event об отказе ссылается на payment span. Metric считает класс route=checkout, outcome=authorization_rejected. Идентификатор конкретного пути остаётся в trace и event.
// Учебный пример. Он не создаёт telemetry и не сообщает о production.\nconst trace = {\n traceId: 'synthetic-trace-2023-09-A',\n spans: [\n { spanId: 'synthetic-span-gateway-A', name: 'checkout', parent: null },\n { spanId: 'synthetic-span-payment-A', name: 'payment.authorize',\n parent: 'synthetic-span-gateway-A' },\n ],\n};\n\nconst metricPoint = {\n name: 'checkout.authorization.rejected.total',\n value: 1,\n labels: {\n service: 'checkout-api',\n route: 'checkout',\n outcome: 'authorization_rejected',\n },\n // trace_id намеренно не является label.\n};\n\nconst event = {\n name: 'payment.authorization.rejected',\n traceId: trace.traceId,\n spanId: 'synthetic-span-payment-A',\n attributes: { failureClass: 'declined' },\n};\nВ примере три labels имеют небольшой словарь только по замыслу. Учебные строки не доказывают, что такой набор безопасен для любого backend. Реальный владелец метрики должен знать допустимые значения, объём данных, правила retention и способ измерения series. Но граница уже видна: trace_id, request_id, user_id, order_id, полный URL и текст исключения описывают отдельные случаи. Их нельзя добавлять в metric labels «на всякий случай».
Trace ID не запрещён во всех местах метрики. OpenTelemetry описывает exemplars как механизм, который может связать измеренное значение с trace и span. Это другой канал связи, не обычная dimension временного ряда. Нельзя заменить exemplar добавлением идентификатора в каждый label и объявить задачи одинаковыми. Конкретная поддержка exemplars зависит от инструмента и backend, поэтому её нужно проверять отдельно.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| График есть, виновный запрос не находится | Метрика должна была заменить trace | Проверить, есть ли рабочая связь от точки измерения к trace или event | Оставить labels агрегируемыми и настроить отдельную корреляцию |
| Число series растёт вместе с трафиком | В labels попал per-request ID или свободный текст | Выписать словарь значений каждого label и найти значения, уникальные для запросов | Убрать поле из labels; перенести его в event attributes или корреляционный механизм |
| Log найден, но относится к другому span | Контекст потерялся на границе процесса или записан вручную | Сравнить trace ID, span ID и родительский путь на одном учебном сценарии | Исправить propagation и формат записи; mismatch считать отрицательным результатом |
| Одна ошибка попала в несколько групп | Названия outcome и route не имеют единого договора | Сопоставить значения с владельцем маршрута и схемой агрегации | Зафиксировать малый словарь и версию изменения |
| Новый label нужен только для поиска | Metric используют как индекс событий | Сформулировать вопрос, который этот label должен отвечать в агрегате | Если вопрос про один запрос, использовать trace/log, а не новую dimension |
Положительный пример легко обманчив. Он показывает, что два объекта можно связать одинаковым ID, но не показывает, что система отвергает неверную связь. Минимальный учебный тест должен принимать согласованный trace и отклонять четыре случая: другой trace ID в event, другой span ID, trace_id в labels и новый неизвестный label. Тест проверяет форму договора. Он не проверяет экспорт, collector, индексацию, sampling, storage или реальную cardinality.
// Учебный псевдокод. Вызовы не обращаются к SDK или сети.\nfunction checkScenario({ traceId, spanId, labels }) {\n if (traceId !== 'synthetic-trace-2023-09-A') return 'reject: trace mismatch';\n if (spanId !== 'synthetic-span-payment-A') return 'reject: span mismatch';\n const allowed = ['service', 'route', 'outcome'];\n if (Object.keys(labels).some((key) => !allowed.includes(key))) {\n return 'reject: metric label contract';\n }\n return 'accept: synthetic correlation contract';\n}\n\ncheckScenario({\n traceId: 'synthetic-trace-2023-09-A',\n spanId: 'synthetic-span-payment-A',\n labels: { service: 'checkout-api', route: 'checkout', outcome: 'authorization_rejected' },\n});\n// accept\n\ncheckScenario({\n traceId: 'synthetic-trace-2023-09-A',\n spanId: 'synthetic-span-payment-A',\n labels: { service: 'checkout-api', route: 'checkout', outcome: 'authorization_rejected', trace_id: '...' },\n});\n// reject: metric label contract\nОтрицательный результат не означает, что любой trace ID в любом представлении запрещён. Он означает, что именно этот учебный contract не разрешает использовать его как dimension метрики. В рабочей системе правило должно жить рядом с инструментированием, а не только в статье. Иначе следующий разработчик изменит labels, а проверка останется зелёной на старом примере.
\nВ статье нет production-телеметрии, реального counter, latency, trace, log, dashboard или запроса к backend. Все значения с префиксом synthetic- служат для объяснения связей. Учебная metric point не измеряет количество отказов. Согласованный trace не доказывает, что propagation работает в приложении. Пройденная функция не доказывает, что exporter доставит запись, collector не изменит её и backend покажет её пользователю.
Высокая cardinality тоже не вычисляется по числу labels в примере. Влияние зависит от множества значений, сочетаний dimensions, периода хранения, агрегации и конкретной платформы. Поэтому отрицательный путь должен продолжаться за пределами кода: измерьте число временных рядов и стоимость выбранного набора в разрешённой среде. Если такой проверки нет, формулировка должна быть «контракт не разрешает поле», а не «система доказанно экономна».
\nЕсли общий trace ID не проходит границу процесса, не компенсируйте это копированием идентификатора в каждую метрику. Сначала проверьте propagation, формат записи и доступность корреляции. Если event содержит чувствительные данные, отдельно решите redaction, retention и права доступа. Связь между сигналами не отменяет требований к данным.
\nРабота готова, когда на одном контролируемом сценарии видны четыре результата: агрегатная метрика содержит только согласованные dimensions; trace показывает ожидаемый путь и разные span ID; event ссылается на тот же trace и правильный span; неверный trace, неверный span и per-request label получают отдельный отказ. Для реального контура дополнительно есть проверка propagation и измерение series в конкретном backend. Если можно показать только зелёный учебный пример, готова модель договора, но не production-настройка наблюдаемости.
\nSpanContext, TraceId, SpanId и span tree.