{ "index": 51, "slug": "editorial-2026-08-practice-end-to-end-observability", "title": "Сквозная наблюдаемость без ложных связей: UI, API и worker", "excerpt": "Как сохранить trace context между UI, API и worker, не превращать trace id в измерение метрики и проверять асинхронную границу по воспроизводимому контракту.", "contentHtml": "
End-to-end тест падает на кнопке оформления заказа. В браузере виден timeout. В API-логе есть принятый запрос. В метрике worker растёт число задач. Но найти один backend-запрос по этому тесту нельзя. Инженер не знает, где оборвалась цепочка: в браузере, proxy, API, очереди или worker. Он меняет timeout и повторяет запуск. Иногда тест проходит. Причина остаётся.
Цена ошибки — не только лишние минуты расследования. Команда может увеличить таймаут, скрыть повторную работу или добавить второй диагностический канал. Если для связи в telemetry попадают email, полный URL или raw user id, локальное удобство превращается в проблему данных и кардинальности. Тезис статьи простой: сквозной сигнал начинается с одного технического context и явных границ. Он не начинается с дашборда и не требует передавать весь payload.
Возьмём один учебный сценарий: пользователь нажал «Оплатить», API принял команду, worker обработал задание. UI и API создают операции с техническим correlation context fixed-trace-7f, worker продолжает его на обработке сообщения. Worker увеличивает metric worker.jobs с низкокардинальными attributes; связь отдельной точки с trace возможна через exemplar, а не через label.
Context связывает позиции в одной операции. Он не превращает сигналы в один формат и не отвечает на все вопросы сразу. Span показывает ход ограниченной операции и её длительность. Log объясняет одно событие и его outcome-class. Metric считает повторяющиеся события по небольшому словарю классов; связь отдельной точки метрики с trace оформляется exemplar-ом, а не новым измерением на каждый trace id. Если записать trace id как label метрики, агрегат начнёт хранить идентификаторы отдельных операций. Это уже не полезная группировка.
У асинхронной границы нужен отдельный контракт. API должен решить, что именно передаётся в сообщение, кто создаёт дочернюю операцию и что делать при отсутствии context. Для producer и consumer надо проверить реальное извлечение и вложение metadata: очередь не обязана сама сохранить parent relation. Если context пуст, разбор заканчивается статусом stop-broken-correlation-context. Нельзя дорисовывать связь по одинаковому имени job или времени запуска.
Correlation и identity решают разные задачи. Correlation отвечает: относятся ли записи к одному пути. Identity отвечает: кто совершил действие. Для первой задачи достаточно технического идентификатора внутри разрешённого контура. Добавлять в span или log пользовательский email «для удобства» нельзя без отдельного назначения, доступа и срока хранения.
Полезно заранее записать allow-list. Для UI это могут быть route-template и request-kind. Для API — operation-name и outcome-class. Для worker — job-kind и outcome-class. Вне списка остаются raw user id, email, phone, свободный текст, полный URL с query и текст исключения. Название поля само по себе ничего не гарантирует. Без ограниченного словаря outcome станет свободным текстом.
const forbidden = new Set(['trace-id', 'email', 'raw-user-id', 'free-text-query']);\nconst traceIds = ['4bf92f3577b34da6a3ce929d0e0e4736', '4bf92f3577b34da6a3ce929d0e0e4736'];\nconst metricAttributes = { job_kind: 'checkout.capture', outcome_class: 'accepted' };\nconst validOutcomes = new Set(['accepted', 'domain-rejected', 'transport-unavailable', 'retryable-processing']);\nconst sameTrace = new Set(traceIds).size === 1;\nconst safeFields = Object.keys(metricAttributes).every((field) => !forbidden.has(field));\nconst knownOutcome = validOutcomes.has(metricAttributes.outcome_class);\nconst result = sameTrace && safeFields && knownOutcome ? 'context-ok-metric-via-exemplar' : 'stop-at-boundary';\nconsole.log(result);Это учебный JavaScript-пример. Он проверяет заранее заданный объект в памяти: одинаковый trace id только у операций, низкокардинальные metric attributes и закрытый словарь outcome. Он не создаёт HTTP-заголовок, не подключает SDK, не отправляет telemetry и не подтверждает состояние production. Его задача — не дать назвать схему готовой, если worker потерял context или поле нарушило границу.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Тест видит timeout, backend-запрос не находится | Context не дошёл до API или не попал в log | Сравнить наличие одного технического context на UI и API | Исправить propagation или остановить расследование на границе |
| UI и API связаны, worker выглядит отдельным | Message boundary не описывает перенос и parent relation | Проверить контракт сообщения и значение context перед обработкой | Назначить владельца перехода; при пустом значении вернуть stop |
| Metric имеет почти отдельную series на каждую операцию | В label попал trace id, raw id или свободный текст | Сверить labels с allow-list и посчитать классы, а не значения | Убрать identity; оставить низкокардинальный class |
| Все отказы помечены одинаково | Transport failure и domain rejection смешаны в error=true | Проверить словарь outcome-class | Разделить transport, domain и retryable processing outcome |
| Есть sampling rule, но нет уверенности в покрытии | План выдаётся за измерение | Найти реальные данные о rate, collector и retention | Назвать правило планом; не делать вывод о latency или стоимости |
Transport failure означает, что граница вызова не завершилась ожидаемо. Domain rejection означает, что запрос дошёл до правила и получил допустимый отрицательный исход. Retryable processing failure означает, что worker может повторить работу. Эти состояния требуют разных действий. Одно поле error=true не говорит, искать ли сеть, бизнес-правило или повтор.
Для учебной схемы достаточно небольшого словаря: transport-unavailable, domain-rejected, retryable-processing, accepted. Не следует добавлять в metric текст исключения. Он может содержать input, URL, имя клиента или случайный идентификатор. Подробный event, если он действительно нужен, должен иметь отдельный канал, доступ и retention. Trace context не является разрешением на хранение payload.
Sampling выбирает объём trace-наблюдений. Cardinality определяет, сколько отдельных серий создаёт metric. Если metric получила raw user id, правило sampling для trace не уменьшает проблему metric. Если worker context потерян, двадцать процентов сохранённых trace не докажут связь с worker. Поэтому sampling записывают рядом с причиной, границей и ожидаемым вопросом. Формулировка «ошибка или 1 из 20» здесь только учебная. Она не сообщает реальную долю, стоимость хранения или полноту покрытия.
Время также нельзя складывать без границ. UI wait, server processing, queue delay и worker execution — разные сегменты. Один root span может скрыть ожидание очереди и retry. Три коротких span могут не показать путь, если context оборвался. Пока не определены часы, события и повторные попытки, статья не делает вывода о bottleneck. Это отрицательный путь: отсутствие данных о границе запрещает уверенный performance claim.
Один и тот же context в трёх литералах не доказывает, что заголовок дойдёт через browser, proxy и очередь. SVG не доказывает наличие SDK. Учебный код не измеряет latency, throughput, error rate или стоимость telemetry. OpenTelemetry и W3C задают терминологию и форматы, но не назначают словарь вашей команды, права доступа, срок хранения и правила редактирования данных.
Нельзя объявлять проблему решённой только потому, что тест снова прошёл. Повторный запуск мог попасть в другую ветку, а retry мог скрыть отказ. Нельзя объявлять metric безопасной только по короткому имени label. Нужны допустимые values и проверка неизвестного значения. Если вопрос требует индивидуального payload, его нельзя протащить в агрегат под видом «диагностики».
Материал готов к отдельному design review, когда для одного пути есть карта UI → API → worker, названный владелец каждой границы, один technical context, allow-list полей и словарь outcome-class. Validator должен вернуть только synthetic-observability-plan-hand-off для полного учебного объекта. Для пустого context он обязан вернуть stop-broken-correlation-context; для forbidden field — stop-forbidden-signal-field. Ни один результат не должен называться deploy, rollout или production success.
После этого готовность системы проверяют уже другими средствами: разрешённым тестом propagation, проверкой редактирования данных, контролем доступа, измерением cardinality и сопоставлением реального trace с запросом. Пока этих доказательств нет, корректный итог — ограниченный hand-off и точный stop, а не красивая легенда о сквозной наблюдаемости.