DarkRiDDeR12 мин

Почему trace ID не должен становиться label метрики

НаблюдаемостьАрхитектура

Проблема начинается, когда одинаковое поле считают одинаково полезным во всех сигналах. Trace ID связывает один путь, поэтому его хочется положить в каждую метрику. Текст ошибки помогает понять event, поэтому его хочется превратить в dimension графика. После этого график и поиск будто становятся удобнее, но модель перестаёт отвечать на простой вопрос: что именно агрегируется, а что описывает отдельное событие. Причина запроса всё равно может не связаться с ошибкой, хотя полей стало больше.

Цена ошибки — не обязательно уже измеренный счёт за storage: этот пакет не видел backend, series, query, retention или load. Цена проектная. Без границы labels команда не может заранее сказать, какие значения допускаются, а reviewer не видит, почему очередной идентификатор опаснее нового outcome. Без границы event attributes один отказ превращается в усреднённую цифру. Без trace correlation доказательство меняет форму на каждом переходе и легко превращается в догадку.

Три data model нельзя заменить одним словом «телеметрия»

OpenTelemetry overview уже разделяла tracing, metrics и logs как разные signals. Это не требование держать три разные базы и не обещание, что они появятся в любом SDK. Это причина задавать разным объектам разные вопросы. Trace моделирует причинный путь и несёт SpanContext; metric data model говорит о streams, time series и attributes; log data model выделяет LogRecord с TraceId, SpanId и Attributes. Инструмент может экспортировать их рядом, но их семантика не становится одинаковой от общего transport.

Поле и его допустимая роль в учебной модели
ПолеTraceMetricLog/eventПричина выбора
trace IDобщий ключ путине входит в labelsсвязь события с путёмидентифицирует один сценарий, а не класс агрегации
span IDидентифицирует шагне входит в labelsуказывает шаг событияотделяет gateway от downstream операции
route templateможет быть атрибутом шагамалый labelможет быть event attributeэто предполагаемо небольшой словарь маршрутов
outcome classstatus шагамалый labelevent attribute с деталямипомогает сравнить небольшой набор результатов
текст ошибки или user IDтолько при отдельной политикене входит в labelsне включён в fixtureможет быть чувствительным, нестабильным или per-request
Учебная схема budget: слева три фиксированных metric labels service, route и outcome образуют небольшой заранее названный словарь; справа trace ID, request ID, user ID и error text перечёркнуты как недопустимые labels и направлены к отдельному trace/log контексту.
Схема показывает проектное различие между малыми dimensions и идентификаторами одного запроса. Она не содержит расчёта настоящих series, не измеряет backend и не устанавливает лимит для какого-либо production-контура.

Cardinality — свойство сочетаний, а не красивое запрещённое слово

Слово cardinality становится полезным, когда рядом есть конкретный договор. В metric data model attributes участвуют в различении dimensions/time series. Поэтому важен не один label сам по себе, а набор возможных значений и комбинаций. `route=synthetic-checkout` и `outcome=synthetic-rejected` в fixture имеют заранее фиксированный словарь. `trace_id=synthetic-trace-2023-09-A` выглядит таким же безобидным, пока не вспомнить его роль: в реальной системе identifier должен различать отдельный путь, поэтому его словарь обычно растёт вместе с запросами.

Нельзя подставлять в статью произвольную формулу «N labels = N series» и объявлять результат измерением. Разные SDK, collectors и backends применяют свои ограничения, aggregation, resource attributes и retention. Учебный contract делает меньше: разрешает три конкретных synthetic labels и отвергает лишние ключи. Это создаёт место для содержательного review. Если кто-то предлагает `merchant_id`, `request_id`, полную URL или текст исключения, он должен объяснить ожидаемый словарь, потребителя, privacy-границу и способ измерить последствия вне fixture.

Correlation проходит через trace и log, не через счётчик

В model code `trace.traceId` равен `log.traceId`, а `log.spanId` равен downstream span ID. Это даёт ясную цепочку: событие `synthetic.payment.authorization-rejected` относится к synthetic payment step, который является дочерним gateway step. Metric record при этом только говорит, что есть единица synthetic отказов по трём маленьким dimensions. Она не должна выдавать точку, где произошёл отказ, и не должна хранить request identity для поиска. Если вам нужен ответ про один запрос, начните с correlation key, а не с новой оси графика.

Event/log attributes отвечают на другой вопрос: какая деталь отказа полезна именно для объяснения? В fixture это synthetic domain, synthetic failure class и synthetic retry advice. Они намеренно не являются labels метрики, и они не копируют реальный exception. В production такой набор должен проходить review на семантику, чувствительность, retention и доступ. Без этих условий даже технически верный trace ID может связать данные, которые не следует хранить или широко показывать.

Fixture охраняет границу, а не эмулирует SDK

Исполнимый код не создаёт Span, LogRecord или Metric через OpenTelemetry SDK. Названия объектов — учебные, фиксированные, и все строки начинаются с `synthetic-`. Это намеренное решение: SDK-имитация легко выглядит как проверка exporter и протокола, хотя здесь нет clock, context propagation, sampler, processor, network, collector или storage. Функция принимает только известные поля и проверяет четыре отношения: trace ID совпадает между trace и log, log относится к нужному span, metric labels имеют ровно три ключа, а per-request identifiers не проходят.

import {
  assembleSyntheticTelemetryScenario,
  runTelemetryFixture,
} from './upgrade-2023-09.mjs';

const scenario = assembleSyntheticTelemetryScenario({
  synthetic: true,
  traceId: 'synthetic-trace-2023-09-A',
  rootSpanId: 'synthetic-span-gateway-A',
  downstreamSpanId: 'synthetic-span-payment-A',
  logTraceId: 'synthetic-trace-2023-09-A',
  logSpanId: 'synthetic-span-payment-A',
  eventName: 'synthetic.payment.authorization-rejected',
  metricLabels: {
    service: 'synthetic-checkout-api',
    route: 'synthetic-checkout',
    outcome: 'synthetic-rejected',
  },
});

const report = runTelemetryFixture();
if (!Object.values(report.assertions).every(Boolean)) throw new Error('fixture failed');
console.log(scenario.evidence.conclusion); // not-a-production-observation

// Не создаёт trace/log/metric, не запускает SDK и не отправляет данные.

node web/scripts/upgrade-2023-09.mjs --verify-fixture

# PASS = contract accepted in memory.
# Не измеряет series, latency, export success или query result.

Отрицательные ветки не утверждают, что настоящий label всегда ошибочен. Они показывают, как зафиксировать решение: `trace_id` и `user_id` в metric labels будут отвергнуты именно этим small-budget contract; другой trace ID или span ID в log не будет считаться correlation. Если команда позже расширит модель, она обязана изменить fixture и текст одновременно. Это лучше, чем тихо поменять набор полей в instrumentation и узнавать о последствиях уже после rollout.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Метрика становится местом для поиска единичного request, но по ней всё равно нельзя восстановить его причинный путь; log и trace не совпадают по ключу.
  2. Причина. Per-request identifier приняли за удобный dimension, а event context и SpanContext не получили отдельный contract.
  3. Проверка. Выпишите у каждого proposed label словарь значений. Отдельно отметьте identifiers, свободный текст и чувствительные поля. В fixture добавьте ключ и убедитесь, что trace ID/user ID отклоняются, а log mismatch не маскируется.
  4. Действие. Оставьте в metric labels только согласованные маленькие dimensions; trace ID проведите через context, log/event record и рабочий способ корреляции.
  5. Проверка границы. До реализации назначьте владельца cardinality budget и владельца event attributes. У них должны быть разные решения, даже если один сервис публикует оба сигнала.
  6. Следующий шаг. На одном безопасном тестовом маршруте измерить реальные последствия выбранного набора в конкретном backend. До этого не называйте budget соблюдённым.

Rollback: сначала остановить расширение, потом решать данные

Если новый label оказался неверным, «удалить его из кода» — не полный rollback. Нужно отдельно выяснить, какие конфигурации и записи успели появиться, какие queries, alerts или dashboards от него зависят и можно ли безопасно остановить дальнейшее создание. Fixture не делает ни одного из этих действий: он возвращает snapshot synthetic contract и прямо пишет `effect=no-system-change`. Такое ограничение предотвращает опасный вывод, будто тест JS умеет удалить данные из telemetry backend.

В настоящем контуре выбор между остановкой, изменением label и миграцией видимости зависит от точной платформы. Возможно, корректнее оставить старую metric и ввести новую отдельно, чтобы не смешать семантики; возможно, важнее немедленно прекратить появление чувствительного атрибута. Статья не выбирает за проект. Она оставляет порядок: зафиксировать, что неверно; сохранить минимальное evidence без новых данных; назначить владельца обратимого действия; затем проверить результат в той среде, где есть фактические записи.

Ограничения и следующий шаг

Ни одна строка пакета не является настоящим telemetry record. Здесь нет trace context, propagation, latency, counter, histogram, logs, metrics, network, collector, backend, dashboard, alert, sampling, query или cardinality measurement. Fixture не отправляет данные и не доказывает, что OpenTelemetry SDK настроен, что IDs совпадут в реальном transport или что выбранные labels допустимы для конкретной команды. Он даёт только повторяемый язык review одного synthetic сценария.

Следующий шаг — завести короткую таблицу для одного instrument: metric name, каждое разрешённое dimension value, владелец, потребитель и запрещённые per-request fields. Рядом записать, какой log/event record должен содержать correlation и какую реальную проверку проведут позже. Если таблица не помещается на одну страницу, задача ещё слишком широкая: сужайте сценарий, а не переносите все подробности в metric labels.

Историческая граница сентября 2023

Все технические утверждения привязаны к официальной specification v1.20.0, выпущенной 7 апреля 2023 и доступной к сентябрю. В частности, текст опирается на уже опубликованные определения SpanContext, TraceId, SpanId, metrics data model и logs data model. Он не называет status конкретной SDK или backend и не использует поздние semantic conventions как будто они были готовым контрактом автора 2023 года.

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

  • OpenTelemetry Specification: Release v1.20.0, 7 апреля 2023 — официальный versioned release, доступный к сентябрю 2023. Версия фиксирует историческую рамку статьи, но не подтверждает, что конкретная система экспортирует или хранит telemetry.
  • OpenTelemetry Specification v1.20.0: Overview — первичный обзор разделяет tracing, metrics, logs, resources и context propagation. Он не предписывает один backend, dashboard или retention policy.
  • OpenTelemetry Specification v1.20.0: Tracing API — описывает SpanContext, TraceId и SpanId как данные, которые могут передаваться в distributed context. Само наличие поля в учебной записи не означает, что контекст дошёл через реальный transport.
  • OpenTelemetry Specification v1.20.0: Metrics Data Model — различает события, streams, time series и attributes. В статье слово label применяется к маленькому договору dimension values; он не измеряет фактическую cardinality какого-либо backend.
  • OpenTelemetry Specification v1.20.0: Logs Data Model — описывает LogRecord, включая TraceId, SpanId и Attributes. Он не доказывает, что event/log запись конкретного сервиса доставлена, индексирована или доступна в поиске.