{ "index": 155, "slug": "editorial-2023-09-mechanism-telemetry-signals", "title": "Почему trace ID не должен становиться label метрики", "excerpt": "Trace, metric и log отвечают на разные вопросы. Разбираем, как сохранить корреляцию одного запроса, не превратить метрику в журнал событий и проверить отрицательный путь.", "contentHtml": "

Симптом знаком: график отказов растёт, но инженер не может назвать конкретный запрос и этап, на котором он сломался. В логах есть похожие сообщения, а трасса либо не находится, либо не связана с ними. Первый быстрый ремонт — добавить trace_id или request_id в labels метрики. График становится фильтруемым, но перестаёт быть хорошим графиком. Цена ошибки — не абстрактная «плохая наблюдаемость». Команда смешивает счётчик с идентичностью одного запроса, раздувает число временных рядов и принимает решение по данным, которые не отвечают на вопрос о причине.

\n

Тезис простой: общий идентификатор нужен для корреляции trace и log/event record, а labels метрики должны описывать небольшой набор групп, по которым допустима агрегация. Один и тот же атрибут может встретиться в нескольких сигналах, но его роль не становится одинаковой. Сначала определите вопрос сигнала. Потом выбирайте поле.

\n

Три сигнала, три вопроса

\n

Trace описывает путь операции. Его узлы — spans: например, gateway, вызов каталога и шаг оплаты. У trace есть TraceId, а у каждого span — собственный SpanId. Так можно связать дочернюю операцию с родительской и пройти от общего маршрута к конкретному шагу.

\n

Metric описывает измеряемый класс поведения во времени. Для неё важны имя инструмента, значение, единица, временной ряд и attributes, которые разделяют поток на dimensions. Вопрос метрики звучит как «сколько отказов было на этом маршруте?» или «какое распределение задержки видит этот класс операций?». Вопрос «какой именно request упал?» относится к другой записи.

\n

Log или 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Становиться единственным источником агрегации
\n
\"Учебная
Учебная иллюстрация разделяет поля для агрегации и поля для корреляции. Она не показывает данные конкретного сервиса, backend или production-нагрузку.
\n

Механизм: корреляция отдельно, агрегация отдельно

\n

Представим учебный маршрут checkout. Gateway принимает запрос и создаёт корневой span. Дочерний span вызывает оплату. Оплата отклоняет авторизацию. Во всех трёх шагах используется один учебный trace ID, но gateway и payment имеют разные span ID. Event об отказе ссылается на payment span. Metric считает класс route=checkout, outcome=authorization_rejected. Идентификатор конкретного пути остаётся в trace и event.

\n
// Учебный пример. Он не создаёт 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 «на всякий случай».

\n

Trace ID не запрещён во всех местах метрики. OpenTelemetry описывает exemplars как механизм, который может связать измеренное значение с trace и span. Это другой канал связи, не обычная dimension временного ряда. Нельзя заменить exemplar добавлением идентификатора в каждый label и объявить задачи одинаковыми. Конкретная поддержка exemplars зависит от инструмента и backend, поэтому её нужно проверять отдельно.

\n

Симптом → причина → проверка → действие

\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
\n

Проверка должна включать отрицательный путь

\n

Положительный пример легко обманчив. Он показывает, что два объекта можно связать одинаковым ID, но не показывает, что система отвергает неверную связь. Минимальный учебный тест должен принимать согласованный trace и отклонять четыре случая: другой trace ID в event, другой span ID, trace_id в labels и новый неизвестный label. Тест проверяет форму договора. Он не проверяет экспорт, collector, индексацию, sampling, storage или реальную cardinality.

\n
// Учебный псевдокод. Вызовы не обращаются к 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

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

\n
  1. Назовите симптом. Запишите, какой вопрос остался без ответа: класс отказов, путь одного запроса или контекст события. Не начинайте с названия инструмента.
  2. Разложите объекты. Для trace выпишите spans и родительские связи. Для metric — имя, value, unit и labels. Для event — имя события, trace ID, span ID и attributes.
  3. Составьте словарь labels. Для каждого dimension укажите допустимые значения и владельца. Отдельно отметьте поля, которые меняются почти на каждый запрос.
  4. Проведите корреляцию. На безопасном учебном сценарии проверьте общий trace ID, соответствующий span ID и путь родитель-потомок. Несовпадение должно быть видимым отказом.
  5. Проверьте отрицательный путь. Добавьте per-request ID в копию metric и измените ID в event. Проверка должна отклонить оба случая по разным причинам.
  6. Проверьте реальный контур отдельно. Уточните, как конкретный SDK переносит context, где backend хранит attributes, поддерживает ли он exemplars и какие ограничения действует для series. Учебный код этого не делает.
  7. Зафиксируйте границу. Запишите, какой сигнал отвечает на какой вопрос, кто владеет схемой и как откатывается изменение instrumentation. Не называйте label budget соблюдённым без измерения в выбранной среде.
\n

Ограничения и отрицательный путь

\n

В статье нет production-телеметрии, реального counter, latency, trace, log, dashboard или запроса к backend. Все значения с префиксом synthetic- служат для объяснения связей. Учебная metric point не измеряет количество отказов. Согласованный trace не доказывает, что propagation работает в приложении. Пройденная функция не доказывает, что exporter доставит запись, collector не изменит её и backend покажет её пользователю.

\n

Высокая cardinality тоже не вычисляется по числу labels в примере. Влияние зависит от множества значений, сочетаний dimensions, периода хранения, агрегации и конкретной платформы. Поэтому отрицательный путь должен продолжаться за пределами кода: измерьте число временных рядов и стоимость выбранного набора в разрешённой среде. Если такой проверки нет, формулировка должна быть «контракт не разрешает поле», а не «система доказанно экономна».

\n

Если общий trace ID не проходит границу процесса, не компенсируйте это копированием идентификатора в каждую метрику. Сначала проверьте propagation, формат записи и доступность корреляции. Если event содержит чувствительные данные, отдельно решите redaction, retention и права доступа. Связь между сигналами не отменяет требований к данным.

\n

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

\n

Работа готова, когда на одном контролируемом сценарии видны четыре результата: агрегатная метрика содержит только согласованные dimensions; trace показывает ожидаемый путь и разные span ID; event ссылается на тот же trace и правильный span; неверный trace, неверный span и per-request label получают отдельный отказ. Для реального контура дополнительно есть проверка propagation и измерение series в конкретном backend. Если можно показать только зелёный учебный пример, готова модель договора, но не production-настройка наблюдаемости.

\n

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

\n"}