Files
progcode/editorial/agent-rewrites/155.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

7 lines
20 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 155,
"slug": "editorial-2023-09-mechanism-telemetry-signals",
"title": "Почему trace ID не должен становиться label метрики",
"excerpt": "Trace, metric и log отвечают на разные вопросы. Разбираем, как сохранить корреляцию одного запроса, не превратить метрику в журнал событий и проверить отрицательный путь.",
"contentHtml": "<p>Симптом знаком: график отказов растёт, но инженер не может назвать конкретный запрос и этап, на котором он сломался. В логах есть похожие сообщения, а трасса либо не находится, либо не связана с ними. Первый быстрый ремонт — добавить <code>trace_id</code> или <code>request_id</code> в labels метрики. График становится фильтруемым, но перестаёт быть хорошим графиком. Цена ошибки — не абстрактная «плохая наблюдаемость». Команда смешивает счётчик с идентичностью одного запроса, раздувает число временных рядов и принимает решение по данным, которые не отвечают на вопрос о причине.</p>\n<p>Тезис простой: общий идентификатор нужен для корреляции trace и log/event record, а labels метрики должны описывать небольшой набор групп, по которым допустима агрегация. Один и тот же атрибут может встретиться в нескольких сигналах, но его роль не становится одинаковой. Сначала определите вопрос сигнала. Потом выбирайте поле.</p>\n<h2>Три сигнала, три вопроса</h2>\n<p>Trace описывает путь операции. Его узлы — spans: например, gateway, вызов каталога и шаг оплаты. У trace есть <code>TraceId</code>, а у каждого span — собственный <code>SpanId</code>. Так можно связать дочернюю операцию с родительской и пройти от общего маршрута к конкретному шагу.</p>\n<p>Metric описывает измеряемый класс поведения во времени. Для неё важны имя инструмента, значение, единица, временной ряд и attributes, которые разделяют поток на dimensions. Вопрос метрики звучит как «сколько отказов было на этом маршруте?» или «какое распределение задержки видит этот класс операций?». Вопрос «какой именно request упал?» относится к другой записи.</p>\n<p>Log или event record фиксирует конкретное событие и его контекст. В него можно положить имя события, класс ошибки, span ID и trace ID, если это разрешено политикой хранения. Такая запись помогает объяснить один отказ. Она не заменяет агрегированную метрику, потому что свободный текст и уникальные идентификаторы плохо отвечают на вопрос о тренде.</p>\n<table><caption>Роль поля в разных сигналах</caption><thead><tr><th scope=\"col\">Сигнал</th><th scope=\"col\">Основной вопрос</th><th scope=\"col\">Подходящие данные</th><th scope=\"col\">Чего не следует требовать</th></tr></thead><tbody><tr><td>Trace</td><td>Какой путь прошла операция?</td><td><code>trace_id</code>, <code>span_id</code>, родительский span, имя операции</td><td>Считать все ошибки и строить долгий тренд</td></tr><tr><td>Metric</td><td>Как меняется класс результата?</td><td>route template, service, outcome, environment</td><td>Хранить идентичность каждого запроса</td></tr><tr><td>Log/event</td><td>Что произошло на одном шаге?</td><td>event name, trace ID, span ID, проверенные attributes</td><td>Становиться единственным источником агрегации</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2023/telemetry-signals-2023-cardinality-budget.svg\" alt=\"Учебная схема бюджета cardinality: небольшие labels метрики отделены от trace ID и контекста события\" loading=\"lazy\" /><figcaption>Учебная иллюстрация разделяет поля для агрегации и поля для корреляции. Она не показывает данные конкретного сервиса, backend или production-нагрузку.</figcaption></figure>\n<h2>Механизм: корреляция отдельно, агрегация отдельно</h2>\n<p>Представим учебный маршрут <code>checkout</code>. Gateway принимает запрос и создаёт корневой span. Дочерний span вызывает оплату. Оплата отклоняет авторизацию. Во всех трёх шагах используется один учебный trace ID, но gateway и payment имеют разные span ID. Event об отказе ссылается на payment span. Metric считает класс <code>route=checkout</code>, <code>outcome=authorization_rejected</code>. Идентификатор конкретного пути остаётся в trace и event.</p>\n<pre><code>// Учебный пример. Он не создаёт 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};</code></pre>\n<p>В примере три labels имеют небольшой словарь только по замыслу. Учебные строки не доказывают, что такой набор безопасен для любого backend. Реальный владелец метрики должен знать допустимые значения, объём данных, правила retention и способ измерения series. Но граница уже видна: <code>trace_id</code>, <code>request_id</code>, <code>user_id</code>, <code>order_id</code>, полный URL и текст исключения описывают отдельные случаи. Их нельзя добавлять в metric labels «на всякий случай».</p>\n<p>Trace ID не запрещён во всех местах метрики. OpenTelemetry описывает exemplars как механизм, который может связать измеренное значение с trace и span. Это другой канал связи, не обычная dimension временного ряда. Нельзя заменить exemplar добавлением идентификатора в каждый label и объявить задачи одинаковыми. Конкретная поддержка exemplars зависит от инструмента и backend, поэтому её нужно проверять отдельно.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика разорванной связи между сигналами</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>График есть, виновный запрос не находится</td><td>Метрика должна была заменить trace</td><td>Проверить, есть ли рабочая связь от точки измерения к trace или event</td><td>Оставить labels агрегируемыми и настроить отдельную корреляцию</td></tr><tr><td>Число series растёт вместе с трафиком</td><td>В labels попал per-request ID или свободный текст</td><td>Выписать словарь значений каждого label и найти значения, уникальные для запросов</td><td>Убрать поле из labels; перенести его в event attributes или корреляционный механизм</td></tr><tr><td>Log найден, но относится к другому span</td><td>Контекст потерялся на границе процесса или записан вручную</td><td>Сравнить trace ID, span ID и родительский путь на одном учебном сценарии</td><td>Исправить propagation и формат записи; mismatch считать отрицательным результатом</td></tr><tr><td>Одна ошибка попала в несколько групп</td><td>Названия outcome и route не имеют единого договора</td><td>Сопоставить значения с владельцем маршрута и схемой агрегации</td><td>Зафиксировать малый словарь и версию изменения</td></tr><tr><td>Новый label нужен только для поиска</td><td>Metric используют как индекс событий</td><td>Сформулировать вопрос, который этот label должен отвечать в агрегате</td><td>Если вопрос про один запрос, использовать trace/log, а не новую dimension</td></tr></tbody></table>\n<h2>Проверка должна включать отрицательный путь</h2>\n<p>Положительный пример легко обманчив. Он показывает, что два объекта можно связать одинаковым ID, но не показывает, что система отвергает неверную связь. Минимальный учебный тест должен принимать согласованный trace и отклонять четыре случая: другой trace ID в event, другой span ID, <code>trace_id</code> в labels и новый неизвестный label. Тест проверяет форму договора. Он не проверяет экспорт, collector, индексацию, sampling, storage или реальную cardinality.</p>\n<pre><code>// Учебный псевдокод. Вызовы не обращаются к 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) =&gt; !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</code></pre>\n<p>Отрицательный результат не означает, что любой trace ID в любом представлении запрещён. Он означает, что именно этот учебный contract не разрешает использовать его как dimension метрики. В рабочей системе правило должно жить рядом с инструментированием, а не только в статье. Иначе следующий разработчик изменит labels, а проверка останется зелёной на старом примере.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Назовите симптом.</strong> Запишите, какой вопрос остался без ответа: класс отказов, путь одного запроса или контекст события. Не начинайте с названия инструмента.</li><li><strong>Разложите объекты.</strong> Для trace выпишите spans и родительские связи. Для metric — имя, value, unit и labels. Для event — имя события, trace ID, span ID и attributes.</li><li><strong>Составьте словарь labels.</strong> Для каждого dimension укажите допустимые значения и владельца. Отдельно отметьте поля, которые меняются почти на каждый запрос.</li><li><strong>Проведите корреляцию.</strong> На безопасном учебном сценарии проверьте общий trace ID, соответствующий span ID и путь родитель-потомок. Несовпадение должно быть видимым отказом.</li><li><strong>Проверьте отрицательный путь.</strong> Добавьте per-request ID в копию metric и измените ID в event. Проверка должна отклонить оба случая по разным причинам.</li><li><strong>Проверьте реальный контур отдельно.</strong> Уточните, как конкретный SDK переносит context, где backend хранит attributes, поддерживает ли он exemplars и какие ограничения действует для series. Учебный код этого не делает.</li><li><strong>Зафиксируйте границу.</strong> Запишите, какой сигнал отвечает на какой вопрос, кто владеет схемой и как откатывается изменение instrumentation. Не называйте label budget соблюдённым без измерения в выбранной среде.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>В статье нет production-телеметрии, реального counter, latency, trace, log, dashboard или запроса к backend. Все значения с префиксом <code>synthetic-</code> служат для объяснения связей. Учебная metric point не измеряет количество отказов. Согласованный trace не доказывает, что propagation работает в приложении. Пройденная функция не доказывает, что exporter доставит запись, collector не изменит её и backend покажет её пользователю.</p>\n<p>Высокая cardinality тоже не вычисляется по числу labels в примере. Влияние зависит от множества значений, сочетаний dimensions, периода хранения, агрегации и конкретной платформы. Поэтому отрицательный путь должен продолжаться за пределами кода: измерьте число временных рядов и стоимость выбранного набора в разрешённой среде. Если такой проверки нет, формулировка должна быть «контракт не разрешает поле», а не «система доказанно экономна».</p>\n<p>Если общий trace ID не проходит границу процесса, не компенсируйте это копированием идентификатора в каждую метрику. Сначала проверьте propagation, формат записи и доступность корреляции. Если event содержит чувствительные данные, отдельно решите redaction, retention и права доступа. Связь между сигналами не отменяет требований к данным.</p>\n<h2>Критерий готовности</h2>\n<p>Работа готова, когда на одном контролируемом сценарии видны четыре результата: агрегатная метрика содержит только согласованные dimensions; trace показывает ожидаемый путь и разные span ID; event ссылается на тот же trace и правильный span; неверный trace, неверный span и per-request label получают отдельный отказ. Для реального контура дополнительно есть проверка propagation и измерение series в конкретном backend. Если можно показать только зелёный учебный пример, готова модель договора, но не production-настройка наблюдаемости.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальное описание категорий telemetry и различий между metrics, traces и logs.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/trace/api/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Tracing API</a> — официальная спецификация <code>SpanContext</code>, <code>TraceId</code>, <code>SpanId</code> и span tree.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/metrics/data-model/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Metrics Data Model</a> — официальная модель metric streams, attributes, time series и exemplars.</li></ul>"}