{ "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 и повтор инцидента после следующего релиза.

\n

End-to-end наблюдаемость начинается не с дашборда. Она начинается с проверяемого контракта связи. UI, API и worker должны передать один контекст по названным границам. Каждая граница должна описывать свой сигнал. Если контекст потерян, система должна остановить сквозной вывод. Похожее имя, соседнее время и одинаковый тип операции не заменяют корреляцию.

\n

Что именно связывает сквозной сигнал

\n

Контекст отвечает на вопрос «к какой цепочке относится операция». В стандарте W3C для этого есть переносимый traceparent с trace-id и parent-id. На практике важен не сам заголовок, а договор: кто его извлекает, кто передаёт дальше, кто создаёт новую связь и что происходит с пустым или неверным значением.

\n

У разных сигналов разные задачи. Span показывает участок операции и его границы. Log объясняет событие и его исход. Metric считает повторяющиеся события по небольшому набору признаков. Общий context помогает перейти от одного объекта к другому, но не делает эти объекты взаимозаменяемыми. Нельзя считать metric доказательством конкретной попытки. Нельзя читать один log как полную историю запроса.

\n

Асинхронная очередь добавляет отдельную границу. API может принять сообщение в одной операции, а worker обработать его позже и повторить несколько раз. Время API, задержка очереди и время worker нельзя сложить без явных часов и правил retry. Если carrier сообщения не определён, связь с worker остаётся гипотезой.

\n
\"Схема
Иллюстрация разделяет две независимые задачи: sampling выбирает наблюдаемые traces, а cardinality ограничивает форму агрегируемых признаков. Одно не исправляет другое.
\n

Сигнал должен отвечать на один вопрос

\n

Перед добавлением поля сформулируйте вопрос. Для span это может быть «какая операция заняла участок пути». Для log — «какой ограниченный исход получил API». Для metric — «сколько задач класса payment завершилось исходом timeout». Если вопрос требует email, полного URL, текста запроса или случайного идентификатора, поле нельзя добавлять в metric label. Такие значения раздувают число series и смешивают диагностику с хранением данных.

\n

Ошибка тоже требует словаря. Transport failure означает, что граница вызова не завершилась ожидаемо. Domain rejection означает, что правило продукта отклонило операцию. Retryable failure означает, что worker может попробовать ещё раз. Одно error=true скроет разницу между ними. Храните ограниченный outcome_class, а подробности оставляйте в защищённом событии с отдельными правилами доступа и хранения.

\n
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}
\n

Это учебный JavaScript-пример. Он не подключается к браузеру, HTTP-клиенту, очереди или telemetry backend и не доказывает свойства production-системы. Его задача — показать fail-closed правило: неизвестное поле не проходит молча, а пустой контекст не получает новый идентификатор только ради красивой связи.

\n

Симптом → причина → проверка → действие

\n
Диагностическая таблица для одного UI → API → worker пути
СимптомПричинаПроверкаДействие
e2e упал, backend-запрос не находитсяUI не передал context или query скрывает егоСравнить carrier на запросе с context в spanОстановить вывод и назначить owner propagation
API и worker имеют одинаковый job nameИмя операции приняли за correlationПроверить trace-id и parent relation, а не текст имениНе связывать события эвристикой
Metric содержит много уникальных labelsВ label попали id, URL или свободный текстПосчитать допустимые значения каждой dimensionsОставить named low-cardinality class или убрать поле
После retry длительность выглядит вдвое большеСложили API, очередь и повтор workerРазделить сегменты и проверить источники времениНе делать latency-вывод до полной модели границ
Trace иногда есть, иногда исчезаетSampling или async carrier не описаныПроверить policy, message headers и absent-context branchНазвать правило отбора и fail-closed поведение
\n

Как работает отрицательный путь

\n

Представим учебный маршрут: UI создаёт trace-7f, API получает тот же контекст, а worker получает сообщение без carrier. В этой точке нельзя подставить «ближайший» trace и нельзя связать worker по имени задачи. Правильный результат — stop-broken-correlation-context. Он не сообщает причину сбоя в production. Он сообщает, какого факта не хватает для сквозного вывода.

\n

Другой отрицательный путь возникает, когда UI добавляет email в список полей, а API и worker остаются корректными. Проверка должна остановиться на запрещённом поле. Sampling не исправляет нарушение: меньший объём trace не меняет характер персонального значения. Переагрегация тоже не оправдывает сбор лишнего поля задним числом.

\n

Третий путь — неизвестный исход. Если API записал свободный текст исключения, metric не должна превращать его в новую series. Сначала ограничьте taxonomy: например, ok, domain_rejected, transport_failure, retryable_failure. Если новый исход нельзя отнести к классу, запишите unknown и отправьте вопрос владельцу словаря. Это сохраняет честность сигнала.

\n

Порядок проверки

\n
  1. Опишите один пользовательский путь: UI → API → очередь → worker. Укажите владельца каждой границы.
  2. Назовите carrier, точки extraction и injection, а также реакцию на отсутствующий и неверный context.
  3. Разведите span, log и metric по вопросам. Не переносите trace-id в metric label.
  4. Составьте allow-list полей и закрытый словарь outcome-классов. Уберите identity и свободный payload.
  5. Разделите UI time, API processing, queue delay и worker execution. Не складывайте интервалы без общей модели часов.
  6. Назовите sampling policy и её границу. Не объявляйте coverage, latency или стоимость без измерения.
  7. Прогоните положительный и три отрицательных варианта: потерянный context, запрещённое поле и неизвестный исход.
  8. Сохраните результат как проверяемый контракт. Если любой stop сработал, не публикуйте end-to-end причину.
\n

Ограничения

\n

Наличие одинакового trace-id ещё не доказывает, что пользователь получил успешный результат. Span может быть экспортирован с задержкой. Log может отсутствовать из-за уровня записи. Metric агрегирует множество операций. Sampling может не сохранить нужную попытку. Прокси может удалить заголовок, а очередь — не поддержать выбранный carrier.

\n

Стандарты задают модели и форматы, но не выбирают taxonomy конкретного продукта, retention, доступ, redaction или стоимость telemetry. Учебная схема не доказывает compliance и не заменяет нагрузочное измерение. Перед внедрением нужен отдельный контракт для каждой границы и проверка реального SDK, collector и хранилища.

\n

Критерий готовности

\n

Механизм готов к следующему инженерному шагу, если независимая проверка получает один и тот же результат: для выбранного пути назван carrier, UI, API и worker несут один контекст, каждый сигнал отвечает на свой вопрос, поля проходят allow-list, sampling описан без выдуманной эффективности, а все отрицательные варианты дают именованный stop. При этом нет заявления о production-латентности, покрытии, релизе или устранённой аварии.

\n

Проверяемые источники

" }