Files
progcode/editorial/agent-rewrites/050.json
T

8 lines
23 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": 50,
"slug": "editorial-2026-08-mechanism-end-to-end-observability",
"title": "End-to-end наблюдаемость: как не перепутать сигналы с доказательством",
"excerpt": "Когда e2e-тест падает, совпадение имён в логах не связывает UI, API и worker. Разбираем propagation, смысл span/log/metric, отрицательный путь и критерий, при котором сквозной вывод можно считать проверяемым.",
"contentHtml": "<p>В e2e-тесте упала отправка заказа. Браузер показал таймаут, API записал ошибку, worker продолжил обрабатывать очередь. Все три записи содержат <code>checkout</code>. Но инженер не знает, относятся ли они к одной попытке. Он тратит время на поиск «медленного сервиса», хотя разрыв мог произойти в propagation. Цена ошибки — ложный вывод, лишний rollback и повтор инцидента после следующего релиза.</p>\n<p>End-to-end наблюдаемость начинается не с дашборда. Она начинается с проверяемого контракта связи. UI, API и worker должны передать один контекст по названным границам. Каждая граница должна описывать свой сигнал. Если контекст потерян, система должна остановить сквозной вывод. Похожее имя, соседнее время и одинаковый тип операции не заменяют корреляцию.</p>\n<h2>Сквозная цепочка — это контракт</h2>\n<p>У цепочки есть три разных объекта: операция, контекст и запись о результате. Операция — действие пользователя или сервиса, например <code>checkout.submit</code>. Контекст сообщает, к какой цепочке относится текущий участок. Запись о результате говорит, что именно произошло на этом участке. Если в лог попал только текст ошибки, он не превращается в доказательство связи с конкретным запросом.</p>\n<p>Стандарт W3C Trace Context описывает HTTP-перенос контекста через <code>traceparent</code>. В формате версии <code>00</code> заголовок содержит версию, <code>trace-id</code>, <code>parent-id</code> и flags. <code>trace-id</code> идентифицирует весь trace, а <code>parent-id</code> — родительский участок. Нулевые идентификаторы и неверный формат недействительны: принимающая сторона не должна считать такой заголовок доказательством связи.</p>\n<p>Это не означает, что один заголовок автоматически пройдёт через всю систему. HTTP-прокси, клиентская библиотека, API gateway и consumer должны иметь договор extraction/injection. Для очереди нужен carrier сообщения: например, отдельное поле headers или metadata. Название carrier, допустимый размер, способ сериализации и поведение при потере должны быть частью контракта, а не устной договорённостью.</p>\n<figure><img src='/assets/editorial/2026/end-to-end-observability-2026-cardinality-sampling-tradeoff.svg' alt='Схема разделяет низкокардинальные классы и индивидуальные поля, а sampling показывает как отдельное правило выбора traces' loading='lazy' /><figcaption>Иллюстрация разделяет две независимые задачи: cardinality ограничивает форму агрегируемых признаков, а sampling выбирает наблюдаемые traces. Меньшая выборка не делает персональное поле допустимым.</figcaption></figure>\n<h2>Три сигнала — три разных вопроса</h2>\n<p>В OpenTelemetry trace описывает путь запроса через приложение, metric — измерение во времени, а log — запись события. Их можно связать общим контекстом, но нельзя подменять один другим. Trace помогает найти участок цепочки. Log объясняет локальный исход. Metric показывает масштаб повторяющегося явления. Ни один из них в одиночку не доказывает, что пользователь увидел ошибку или что worker обработал именно этот заказ.</p>\n<p>Сначала сформулируйте вопрос, затем выберите сигнал. Для span вопрос звучит так: «какой участок пути занял время или завершился ошибкой?». Для log: «какой ограниченный исход получил API?». Для metric: «сколько операций класса <code>payment</code> завершилось исходом <code>timeout</code>?». Если ответ требует email, полного URL, свободного текста или случайного идентификатора, такое значение нельзя превращать в metric label.</p>\n<p>У исходов должен быть небольшой словарь. <code>transport_failure</code> означает отказ границы вызова, <code>domain_rejected</code> — отказ правила продукта, <code>retryable_failure</code> — возможность повторной обработки. Одно <code>error=true</code> скроет важное различие. Подробности исключения оставляйте в защищённом событии с правилами доступа и хранения, а в агрегате сохраняйте ограниченный <code>outcome_class</code>.</p>\n<h2>Propagation через HTTP и очередь</h2>\n<p>Проверяйте не наличие похожих строк, а переход между границами. На входе API нужно зафиксировать, какой carrier извлечён и какой trace-id получен. На исходящем запросе API должен создать дочерний span и передать контекст дальше. При постановке сообщения в очередь тот же контекст нужно положить в согласованный carrier. Worker извлекает его и создаёт свой участок обработки; он не должен искать ближайший trace по имени задачи или времени.</p>\n<p>Асинхронная граница меняет смысл времени. API может завершиться через 80 мс, сообщение ждать 2 секунды, а worker выполнить повтор через 300 мс. Это минимум три интервала: обработка API, задержка очереди и выполнение worker. Повтор создаёт новый участок работы, но не обязательно новый пользовательский trace. Если не записать номер попытки и причину retry, суммарная длительность будет выглядеть как один длинный вызов и приведёт к неверной оптимизации.</p>\n<p>Проверка должна различать отсутствие контекста и отказ telemetry backend. В первом случае система не знает, к чему привязать событие. Во втором контекст мог быть корректным, но запись не дошла до хранилища. Обе ситуации ухудшают расследование, однако исправляются на разных границах: в первом ищут extraction/injection, во втором — экспорт, очередь telemetry и retention.</p>\n<h2>Воспроизводимая проверка формата и словаря</h2>\n<p>Ниже — самостоятельный JavaScript-пример без SDK. Он проверяет только две вещи: минимальную структуру W3C-заголовка версии <code>00</code> и закрытый словарь исходов. Значения идентификаторов взяты из примера спецификации; они не являются идентификатором реального пользователя или запроса.</p>\n<pre><code>const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';\nconst allowedOutcomes = new Set([\n 'ok', 'domain_rejected', 'transport_failure', 'retryable_failure'\n]);\n\nfunction nonZero(value) {\n return /[1-9a-f]/.test(value);\n}\n\nfunction validTraceparent(value) {\n const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec(value);\n return Boolean(match) &amp;&amp; nonZero(match[1]) &amp;&amp; nonZero(match[2]);\n}\n\nfunction inspectSignal(signal) {\n if (!validTraceparent(signal.traceparent)) {\n return { ok: false, reason: 'stop-broken-correlation-context' };\n }\n if (!allowedOutcomes.has(signal.outcome_class)) {\n return { ok: false, reason: 'stop-unknown-outcome-class' };\n }\n return { ok: true, traceparent: signal.traceparent,\n operation: signal.operation, outcome_class: signal.outcome_class };\n}\n\nconsole.log(inspectSignal({\n traceparent,\n operation: 'checkout.submit',\n outcome_class: 'transport_failure'\n}));</code></pre>\n<p>После декодирования HTML этот код можно запустить в обычном Node.js. Валидный пример возвращает <code>ok: true</code>. Если заменить последний фрагмент заголовка на <code>00</code>, формат останется допустимым, но sampled-флаг будет снят; это не доказательство отсутствия trace. Если удалить один символ из <code>trace-id</code>, функция вернёт именованный stop. Если подставить свободный текст вместо <code>outcome_class</code>, сработает второй stop. Так проверяется не «красивый лог», а заранее названное правило.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><caption>Диагностическая таблица для одного UI → API → worker пути</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>e2e упал, backend-запрос не находится</td><td>UI не передал context или query скрывает его</td><td>Сравнить carrier на запросе с context в span</td><td>Остановить вывод и назначить владельца propagation</td></tr><tr><td>API и worker имеют одинаковый job name</td><td>Имя операции приняли за correlation</td><td>Проверить trace-id, parent relation и attempt</td><td>Не связывать события эвристикой</td></tr><tr><td>Metric содержит много уникальных labels</td><td>В label попали id, URL или свободный текст</td><td>Посчитать допустимые значения каждой dimension</td><td>Оставить low-cardinality class или убрать поле</td></tr><tr><td>После retry длительность выглядит вдвое больше</td><td>Сложили API, очередь и повтор worker</td><td>Разделить сегменты и проверить источники времени</td><td>Не делать latency-вывод без модели границ</td></tr><tr><td>Trace иногда есть, иногда исчезает</td><td>Sampling или async carrier не описаны</td><td>Проверить policy, message headers и absent-context branch</td><td>Назвать правило отбора и fail-closed поведение</td></tr></tbody></table></div>\n<h2>Отрицательный путь важнее счастливого</h2>\n<p>Представим учебный маршрут: UI создаёт корректный <code>traceparent</code>, API получает его и публикует сообщение, а worker получает сообщение без carrier. В этой точке нельзя подставить «ближайший» trace и нельзя связать worker по имени задачи. Правильный результат — <code>stop-broken-correlation-context</code>. Он не утверждает причину сбоя в продукте; он сообщает, какого факта не хватает для сквозного вывода.</p>\n<p>Второй отрицательный путь — посредник удалил заголовок, но API создал новый trace и продолжил работу. Для локальной диагностики это может быть допустимым решением, но в отчёте нужно отметить разрыв: новый trace не доказывает продолжение исходного. Если граница критична, лучше сохранить отдельное событие о потере связи и передать в доменную команду только явно разрешённые признаки.</p>\n<p>Третий путь — неизвестный исход. Если API записал свободный текст исключения, metric не должна превращать каждую новую строку в series. Сначала ограничьте taxonomy. Если новый исход нельзя отнести к классу, запишите <code>unknown</code> или остановите публикацию метрики по правилу команды, а затем обновите словарь. Нельзя ретроспективно выдавать неизвестное значение за известный класс.</p>\n<h2>Sampling и cardinality не заменяют корреляцию</h2>\n<p>Sampling отвечает на вопрос «какие traces записывать или экспортировать». Cardinality отвечает на вопрос «сколько различных значений может иметь признак в агрегате». Это разные оси стоимости и качества. Можно выбрать только один trace из двадцати и всё равно создать опасную series для каждого email. Можно ограничить label словарём и всё равно потерять нужную попытку из-за sampling.</p>\n<p>Флаг <code>sampled</code> в <code>traceparent</code> сообщает о решении caller записывать trace, но не превращается в гарантию, что все downstream-системы сохранили данные. Компонент может изменить решение из-за своей нагрузки или политики. Поэтому в расследовании проверяйте фактическое наличие span и конфигурацию экспортёра, а не только значение флага.</p>\n<p>Не обещайте покрытие или экономию без измерения. Для решения нужны хотя бы объём входных операций, доля сохранённых traces, число series по каждой dimension, размер событий и срок хранения. Эти значения зависят от SDK, collector, backend, нагрузки и правил redaction. Перенос цифры из чужого окружения не делает её результатом вашего измерения.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Опишите один путь: UI → API → очередь → worker. Укажите владельца каждой границы и отдельно обозначьте синхронные и асинхронные участки.</li><li>Назовите carrier, точки extraction и injection, формат значения и реакцию на отсутствующий или неверный context.</li><li>Для одной тестовой попытки выпишите trace-id, parent-id, attempt и outcome. Сверьте их в запросе, сообщении и записи worker.</li><li>Разведите span, log и metric по вопросам. Не переносите trace-id, email, raw id или свободный payload в metric label.</li><li>Составьте allow-list полей и закрытый словарь outcome-классов. Зафиксируйте, где хранится подробное исключение и кто имеет к нему доступ.</li><li>Разделите UI time, API processing, queue delay и worker execution. Для retry показывайте номер попытки и не складывайте интервалы без общей модели часов.</li><li>Назовите sampling policy, границу её применения и способ проверки фактической записи. Не называйте флаг <code>sampled</code> доказательством сохранения.</li><li>Прогоните положительный и три отрицательных варианта: потерянный carrier, неверный заголовок, запрещённое поле и неизвестный исход.</li><li>Сохраните результат как проверяемый контракт. Если любой stop сработал, не публикуйте end-to-end причину и не заменяйте её догадкой.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Одинаковый trace-id не доказывает, что пользователь получил успешный результат. Span может быть экспортирован с задержкой, log — отброшен уровнем записи, metric — агрегировать множество операций, а sampling — не сохранить нужную попытку. Прокси может удалить заголовок, очередь — не поддержать выбранный carrier, а retry — породить несколько обработок одного сообщения.</p>\n<p>W3C задаёт формат trace context, а OpenTelemetry — модель сигналов и контекстов; эти документы не выбирают taxonomy продукта, retention, redaction, права доступа, SLA freshness или стоимость telemetry. Учебный код не подключается к браузеру, HTTP-клиенту, очереди или backend. Он проверяет локальный инвариант и не заменяет интеграционный тест с реальным SDK и collector.</p>\n<p>Для защищённых данных корреляция не должна становиться способом передать персональные сведения. Trace-id обычно безопаснее email, но он всё равно может связать записи между системами. Опишите срок хранения, доступ, маскирование и процедуру удаления отдельно. Если эти правила не определены, полезность подробного контекста не оправдывает его сбор.</p>\n<h2>Критерий готовности</h2>\n<p>Механизм готов к следующему инженерному шагу, если независимая проверка получает один и тот же результат: для выбранного пути назван carrier, UI, API и worker несут проверяемый контекст, каждый сигнал отвечает на свой вопрос, поля проходят allow-list, sampling описан без выдуманной эффективности, а отрицательные варианты дают именованный stop. При этом нет заявления о production-латентности, покрытии, релизе или устранённой аварии без соответствующего измерения.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://opentelemetry.io/docs/concepts/signals/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Signals</a> — официальное описание traces, metrics, logs и baggage. Источник задаёт терминологию сигналов, но не подтверждает наличие telemetry в конкретной системе.</li><li><a href='https://opentelemetry.io/docs/concepts/context-propagation/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Context propagation</a> — официальное описание передачи контекста между сервисами и границами. Страница не задаёт ваш carrier очереди, retention или политику персональных данных.</li><li><a href='https://www.w3.org/TR/trace-context/' target='_blank' rel='noopener noreferrer'>W3C Trace Context</a> — официальная рекомендация для trace context, заголовков <code>traceparent</code> и trace flags. Документ не доказывает propagation через конкретные proxy, API или queue.</li></ul>"
}