8 lines
23 KiB
JSON
8 lines
23 KiB
JSON
{
|
||
"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) && nonZero(match[1]) && 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>"
|
||
}
|