8 lines
15 KiB
JSON
8 lines
15 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>Контекст отвечает на вопрос «к какой цепочке относится операция». В стандарте W3C для этого есть переносимый <code>traceparent</code> с trace-id и parent-id. На практике важен не сам заголовок, а договор: кто его извлекает, кто передаёт дальше, кто создаёт новую связь и что происходит с пустым или неверным значением.</p>\n<p>У разных сигналов разные задачи. Span показывает участок операции и его границы. Log объясняет событие и его исход. Metric считает повторяющиеся события по небольшому набору признаков. Общий context помогает перейти от одного объекта к другому, но не делает эти объекты взаимозаменяемыми. Нельзя считать metric доказательством конкретной попытки. Нельзя читать один log как полную историю запроса.</p>\n<p>Асинхронная очередь добавляет отдельную границу. API может принять сообщение в одной операции, а worker обработать его позже и повторить несколько раз. Время API, задержка очереди и время worker нельзя сложить без явных часов и правил retry. Если carrier сообщения не определён, связь с worker остаётся гипотезой.</p>\n<figure><img src=\"/assets/editorial/2026/end-to-end-observability-2026-cardinality-sampling-tradeoff.svg\" alt=\"Схема связи сквозного контекста с сигналами и границами cardinality и sampling\" loading=\"lazy\" /><figcaption>Иллюстрация разделяет две независимые задачи: sampling выбирает наблюдаемые traces, а cardinality ограничивает форму агрегируемых признаков. Одно не исправляет другое.</figcaption></figure>\n<h2>Сигнал должен отвечать на один вопрос</h2>\n<p>Перед добавлением поля сформулируйте вопрос. Для span это может быть «какая операция заняла участок пути». Для log — «какой ограниченный исход получил API». Для metric — «сколько задач класса <code>payment</code> завершилось исходом <code>timeout</code>». Если вопрос требует email, полного URL, текста запроса или случайного идентификатора, поле нельзя добавлять в metric label. Такие значения раздувают число series и смешивают диагностику с хранением данных.</p>\n<p>Ошибка тоже требует словаря. Transport failure означает, что граница вызова не завершилась ожидаемо. Domain rejection означает, что правило продукта отклонило операцию. Retryable failure означает, что worker может попробовать ещё раз. Одно <code>error=true</code> скроет разницу между ними. Храните ограниченный <code>outcome_class</code>, а подробности оставляйте в защищённом событии с отдельными правилами доступа и хранения.</p>\n<pre><code>const signal = {\n context: 'trace-7f',\n operation: 'checkout.submit',\n outcome_class: 'transport_failure',\n route_template: '/orders/{id}'\n};\n\nconst allowed = new Set([\n 'operation', 'outcome_class', 'route_template'\n]);\n\nfunction accept(fields) {\n return Object.keys(fields).every((name) => allowed.has(name));\n}\n\nif (!accept(signal)) {\n throw new Error('stop: forbidden signal field');\n}</code></pre>\n<p>Это учебный JavaScript-пример. Он не подключается к браузеру, HTTP-клиенту, очереди или telemetry backend и не доказывает свойства production-системы. Его задача — показать fail-closed правило: неизвестное поле не проходит молча, а пустой контекст не получает новый идентификатор только ради красивой связи.</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>Остановить вывод и назначить owner propagation</td></tr><tr><td>API и worker имеют одинаковый job name</td><td>Имя операции приняли за correlation</td><td>Проверить trace-id и parent relation, а не текст имени</td><td>Не связывать события эвристикой</td></tr><tr><td>Metric содержит много уникальных labels</td><td>В label попали id, URL или свободный текст</td><td>Посчитать допустимые значения каждой dimensions</td><td>Оставить named 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>trace-7f</code>, API получает тот же контекст, а worker получает сообщение без carrier. В этой точке нельзя подставить «ближайший» trace и нельзя связать worker по имени задачи. Правильный результат — <code>stop-broken-correlation-context</code>. Он не сообщает причину сбоя в production. Он сообщает, какого факта не хватает для сквозного вывода.</p>\n<p>Другой отрицательный путь возникает, когда UI добавляет <code>email</code> в список полей, а API и worker остаются корректными. Проверка должна остановиться на запрещённом поле. Sampling не исправляет нарушение: меньший объём trace не меняет характер персонального значения. Переагрегация тоже не оправдывает сбор лишнего поля задним числом.</p>\n<p>Третий путь — неизвестный исход. Если API записал свободный текст исключения, metric не должна превращать его в новую series. Сначала ограничьте taxonomy: например, <code>ok</code>, <code>domain_rejected</code>, <code>transport_failure</code>, <code>retryable_failure</code>. Если новый исход нельзя отнести к классу, запишите <code>unknown</code> и отправьте вопрос владельцу словаря. Это сохраняет честность сигнала.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Опишите один пользовательский путь: UI → API → очередь → worker. Укажите владельца каждой границы.</li><li>Назовите carrier, точки extraction и injection, а также реакцию на отсутствующий и неверный context.</li><li>Разведите span, log и metric по вопросам. Не переносите trace-id в metric label.</li><li>Составьте allow-list полей и закрытый словарь outcome-классов. Уберите identity и свободный payload.</li><li>Разделите UI time, API processing, queue delay и worker execution. Не складывайте интервалы без общей модели часов.</li><li>Назовите sampling policy и её границу. Не объявляйте coverage, latency или стоимость без измерения.</li><li>Прогоните положительный и три отрицательных варианта: потерянный context, запрещённое поле и неизвестный исход.</li><li>Сохраните результат как проверяемый контракт. Если любой stop сработал, не публикуйте end-to-end причину.</li></ol>\n<h2>Ограничения</h2>\n<p>Наличие одинакового trace-id ещё не доказывает, что пользователь получил успешный результат. Span может быть экспортирован с задержкой. Log может отсутствовать из-за уровня записи. Metric агрегирует множество операций. Sampling может не сохранить нужную попытку. Прокси может удалить заголовок, а очередь — не поддержать выбранный carrier.</p>\n<p>Стандарты задают модели и форматы, но не выбирают taxonomy конкретного продукта, retention, доступ, redaction или стоимость telemetry. Учебная схема не доказывает compliance и не заменяет нагрузочное измерение. Перед внедрением нужен отдельный контракт для каждой границы и проверка реального SDK, collector и хранилища.</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. Источник задаёт терминологию сигналов, но не подтверждает наличие telemetry в конкретной системе.</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>. Документ не доказывает propagation через конкретный proxy, API или очередь.</li></ul>"
|
||
}
|