{ "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": "
В e2e-тесте упала отправка заказа. Браузер показал таймаут, API записал ошибку, worker продолжил обрабатывать очередь. Все три записи содержат checkout. Но инженер не знает, относятся ли они к одной попытке. Он тратит время на поиск «медленного сервиса», хотя разрыв мог произойти в propagation. Цена ошибки — ложный вывод, лишний rollback и повтор инцидента после следующего релиза.
End-to-end наблюдаемость начинается не с дашборда. Она начинается с проверяемого контракта связи. UI, API и worker должны передать один контекст по названным границам. Каждая граница должна описывать свой сигнал. Если контекст потерян, система должна остановить сквозной вывод. Похожее имя, соседнее время и одинаковый тип операции не заменяют корреляцию.
\nУ цепочки есть три разных объекта: операция, контекст и запись о результате. Операция — действие пользователя или сервиса, например checkout.submit. Контекст сообщает, к какой цепочке относится текущий участок. Запись о результате говорит, что именно произошло на этом участке. Если в лог попал только текст ошибки, он не превращается в доказательство связи с конкретным запросом.
Стандарт W3C Trace Context описывает HTTP-перенос контекста через traceparent. В формате версии 00 заголовок содержит версию, trace-id, parent-id и flags. trace-id идентифицирует весь trace, а parent-id — родительский участок. Нулевые идентификаторы и неверный формат недействительны: принимающая сторона не должна считать такой заголовок доказательством связи.
Это не означает, что один заголовок автоматически пройдёт через всю систему. HTTP-прокси, клиентская библиотека, API gateway и consumer должны иметь договор extraction/injection. Для очереди нужен carrier сообщения: например, отдельное поле headers или metadata. Название carrier, допустимый размер, способ сериализации и поведение при потере должны быть частью контракта, а не устной договорённостью.
\nВ OpenTelemetry trace описывает путь запроса через приложение, metric — измерение во времени, а log — запись события. Их можно связать общим контекстом, но нельзя подменять один другим. Trace помогает найти участок цепочки. Log объясняет локальный исход. Metric показывает масштаб повторяющегося явления. Ни один из них в одиночку не доказывает, что пользователь увидел ошибку или что worker обработал именно этот заказ.
\nСначала сформулируйте вопрос, затем выберите сигнал. Для span вопрос звучит так: «какой участок пути занял время или завершился ошибкой?». Для log: «какой ограниченный исход получил API?». Для metric: «сколько операций класса payment завершилось исходом timeout?». Если ответ требует email, полного URL, свободного текста или случайного идентификатора, такое значение нельзя превращать в metric label.
У исходов должен быть небольшой словарь. transport_failure означает отказ границы вызова, domain_rejected — отказ правила продукта, retryable_failure — возможность повторной обработки. Одно error=true скроет важное различие. Подробности исключения оставляйте в защищённом событии с правилами доступа и хранения, а в агрегате сохраняйте ограниченный outcome_class.
Проверяйте не наличие похожих строк, а переход между границами. На входе API нужно зафиксировать, какой carrier извлечён и какой trace-id получен. На исходящем запросе API должен создать дочерний span и передать контекст дальше. При постановке сообщения в очередь тот же контекст нужно положить в согласованный carrier. Worker извлекает его и создаёт свой участок обработки; он не должен искать ближайший trace по имени задачи или времени.
\nАсинхронная граница меняет смысл времени. API может завершиться через 80 мс, сообщение ждать 2 секунды, а worker выполнить повтор через 300 мс. Это минимум три интервала: обработка API, задержка очереди и выполнение worker. Повтор создаёт новый участок работы, но не обязательно новый пользовательский trace. Если не записать номер попытки и причину retry, суммарная длительность будет выглядеть как один длинный вызов и приведёт к неверной оптимизации.
\nПроверка должна различать отсутствие контекста и отказ telemetry backend. В первом случае система не знает, к чему привязать событие. Во втором контекст мог быть корректным, но запись не дошла до хранилища. Обе ситуации ухудшают расследование, однако исправляются на разных границах: в первом ищут extraction/injection, во втором — экспорт, очередь telemetry и retention.
\nНиже — самостоятельный JavaScript-пример без SDK. Он проверяет только две вещи: минимальную структуру W3C-заголовка версии 00 и закрытый словарь исходов. Значения идентификаторов взяты из примера спецификации; они не являются идентификатором реального пользователя или запроса.
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}));\nПосле декодирования HTML этот код можно запустить в обычном Node.js. Валидный пример возвращает ok: true. Если заменить последний фрагмент заголовка на 00, формат останется допустимым, но sampled-флаг будет снят; это не доказательство отсутствия trace. Если удалить один символ из trace-id, функция вернёт именованный stop. Если подставить свободный текст вместо outcome_class, сработает второй stop. Так проверяется не «красивый лог», а заранее названное правило.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| e2e упал, backend-запрос не находится | UI не передал context или query скрывает его | Сравнить carrier на запросе с context в span | Остановить вывод и назначить владельца propagation |
| API и worker имеют одинаковый job name | Имя операции приняли за correlation | Проверить trace-id, parent relation и attempt | Не связывать события эвристикой |
| Metric содержит много уникальных labels | В label попали id, URL или свободный текст | Посчитать допустимые значения каждой dimension | Оставить low-cardinality class или убрать поле |
| После retry длительность выглядит вдвое больше | Сложили API, очередь и повтор worker | Разделить сегменты и проверить источники времени | Не делать latency-вывод без модели границ |
| Trace иногда есть, иногда исчезает | Sampling или async carrier не описаны | Проверить policy, message headers и absent-context branch | Назвать правило отбора и fail-closed поведение |
Представим учебный маршрут: UI создаёт корректный traceparent, API получает его и публикует сообщение, а worker получает сообщение без carrier. В этой точке нельзя подставить «ближайший» trace и нельзя связать worker по имени задачи. Правильный результат — stop-broken-correlation-context. Он не утверждает причину сбоя в продукте; он сообщает, какого факта не хватает для сквозного вывода.
Второй отрицательный путь — посредник удалил заголовок, но API создал новый trace и продолжил работу. Для локальной диагностики это может быть допустимым решением, но в отчёте нужно отметить разрыв: новый trace не доказывает продолжение исходного. Если граница критична, лучше сохранить отдельное событие о потере связи и передать в доменную команду только явно разрешённые признаки.
\nТретий путь — неизвестный исход. Если API записал свободный текст исключения, metric не должна превращать каждую новую строку в series. Сначала ограничьте taxonomy. Если новый исход нельзя отнести к классу, запишите unknown или остановите публикацию метрики по правилу команды, а затем обновите словарь. Нельзя ретроспективно выдавать неизвестное значение за известный класс.
Sampling отвечает на вопрос «какие traces записывать или экспортировать». Cardinality отвечает на вопрос «сколько различных значений может иметь признак в агрегате». Это разные оси стоимости и качества. Можно выбрать только один trace из двадцати и всё равно создать опасную series для каждого email. Можно ограничить label словарём и всё равно потерять нужную попытку из-за sampling.
\nФлаг sampled в traceparent сообщает о решении caller записывать trace, но не превращается в гарантию, что все downstream-системы сохранили данные. Компонент может изменить решение из-за своей нагрузки или политики. Поэтому в расследовании проверяйте фактическое наличие span и конфигурацию экспортёра, а не только значение флага.
Не обещайте покрытие или экономию без измерения. Для решения нужны хотя бы объём входных операций, доля сохранённых traces, число series по каждой dimension, размер событий и срок хранения. Эти значения зависят от SDK, collector, backend, нагрузки и правил redaction. Перенос цифры из чужого окружения не делает её результатом вашего измерения.
\nsampled доказательством сохранения.Одинаковый trace-id не доказывает, что пользователь получил успешный результат. Span может быть экспортирован с задержкой, log — отброшен уровнем записи, metric — агрегировать множество операций, а sampling — не сохранить нужную попытку. Прокси может удалить заголовок, очередь — не поддержать выбранный carrier, а retry — породить несколько обработок одного сообщения.
\nW3C задаёт формат trace context, а OpenTelemetry — модель сигналов и контекстов; эти документы не выбирают taxonomy продукта, retention, redaction, права доступа, SLA freshness или стоимость telemetry. Учебный код не подключается к браузеру, HTTP-клиенту, очереди или backend. Он проверяет локальный инвариант и не заменяет интеграционный тест с реальным SDK и collector.
\nДля защищённых данных корреляция не должна становиться способом передать персональные сведения. Trace-id обычно безопаснее email, но он всё равно может связать записи между системами. Опишите срок хранения, доступ, маскирование и процедуру удаления отдельно. Если эти правила не определены, полезность подробного контекста не оправдывает его сбор.
\nМеханизм готов к следующему инженерному шагу, если независимая проверка получает один и тот же результат: для выбранного пути назван carrier, UI, API и worker несут проверяемый контекст, каждый сигнал отвечает на свой вопрос, поля проходят allow-list, sampling описан без выдуманной эффективности, а отрицательные варианты дают именованный stop. При этом нет заявления о production-латентности, покрытии, релизе или устранённой аварии без соответствующего измерения.
\ntraceparent и trace flags. Документ не доказывает propagation через конкретные proxy, API или queue.