8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 51,
|
||
"slug": "editorial-2026-08-practice-end-to-end-observability",
|
||
"title": "Сквозная наблюдаемость без ложных связей: UI, API и worker",
|
||
"excerpt": "Как сохранить trace context между UI, API и worker, не превращать trace id в измерение метрики и проверять асинхронную границу по воспроизводимому контракту.",
|
||
"contentHtml": "<p>End-to-end тест падает на кнопке оформления заказа. В браузере виден timeout. В API-логе есть принятый запрос. В метрике worker растёт число задач. Но найти один backend-запрос по этому тесту нельзя. Инженер не знает, где оборвалась цепочка: в браузере, proxy, API, очереди или worker. Он меняет timeout и повторяет запуск. Иногда тест проходит. Причина остаётся.</p><p>Цена ошибки — не только лишние минуты расследования. Команда может увеличить таймаут, скрыть повторную работу или добавить второй диагностический канал. Если для связи в telemetry попадают email, полный URL или raw user id, локальное удобство превращается в проблему данных и кардинальности. Тезис статьи простой: сквозной сигнал начинается с одного технического context и явных границ. Он не начинается с дашборда и не требует передавать весь payload.</p><h2>Механизм: один путь, три разных сигнала</h2><p>Возьмём один учебный сценарий: пользователь нажал «Оплатить», API принял команду, worker обработал задание. UI и API создают операции с техническим correlation context <code>fixed-trace-7f</code>, worker продолжает его на обработке сообщения. Worker увеличивает metric <code>worker.jobs</code> с низкокардинальными attributes; связь отдельной точки с trace возможна через exemplar, а не через label.</p><p>Context связывает позиции в одной операции. Он не превращает сигналы в один формат и не отвечает на все вопросы сразу. Span показывает ход ограниченной операции и её длительность. Log объясняет одно событие и его outcome-class. Metric считает повторяющиеся события по небольшому словарю классов; связь отдельной точки метрики с trace оформляется exemplar-ом, а не новым измерением на каждый trace id. Если записать trace id как label метрики, агрегат начнёт хранить идентификаторы отдельных операций. Это уже не полезная группировка.</p><p>У асинхронной границы нужен отдельный контракт. API должен решить, что именно передаётся в сообщение, кто создаёт дочернюю операцию и что делать при отсутствии context. Для producer и consumer надо проверить реальное извлечение и вложение metadata: очередь не обязана сама сохранить parent relation. Если context пуст, разбор заканчивается статусом <code>stop-broken-correlation-context</code>. Нельзя дорисовывать связь по одинаковому имени job или времени запуска.</p><figure><img src=\"/assets/editorial/2026/end-to-end-observability-2026-signal-boundary-map.svg\" alt=\"Карта границ сигнала между UI, API и worker с техническим context, типами сигналов и запрещёнными полями\" loading=\"lazy\" /><figcaption>Учебная карта показывает владельца каждого перехода. Она не изображает работающую telemetry-систему, не содержит production trace и не доказывает propagation через конкретную очередь.</figcaption></figure><h2>Что разрешает context</h2><p>Correlation и identity решают разные задачи. Correlation отвечает: относятся ли записи к одному пути. Identity отвечает: кто совершил действие. Для первой задачи достаточно технического идентификатора внутри разрешённого контура. Добавлять в span или log пользовательский email «для удобства» нельзя без отдельного назначения, доступа и срока хранения.</p><p>Полезно заранее записать allow-list. Для UI это могут быть <code>route-template</code> и <code>request-kind</code>. Для API — <code>operation-name</code> и <code>outcome-class</code>. Для worker — <code>job-kind</code> и <code>outcome-class</code>. Вне списка остаются raw user id, email, phone, свободный текст, полный URL с query и текст исключения. Название поля само по себе ничего не гарантирует. Без ограниченного словаря <code>outcome</code> станет свободным текстом.</p><pre><code>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);</code></pre><p>Это учебный JavaScript-пример. Он проверяет заранее заданный объект в памяти: одинаковый trace id только у операций, низкокардинальные metric attributes и закрытый словарь outcome. Он не создаёт HTTP-заголовок, не подключает SDK, не отправляет telemetry и не подтверждает состояние production. Его задача — не дать назвать схему готовой, если worker потерял context или поле нарушило границу.</p><h2>Симптом → причина → проверка → действие</h2><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>Тест видит timeout, backend-запрос не находится</td><td>Context не дошёл до API или не попал в log</td><td>Сравнить наличие одного технического context на UI и API</td><td>Исправить propagation или остановить расследование на границе</td></tr><tr><td>UI и API связаны, worker выглядит отдельным</td><td>Message boundary не описывает перенос и parent relation</td><td>Проверить контракт сообщения и значение context перед обработкой</td><td>Назначить владельца перехода; при пустом значении вернуть stop</td></tr><tr><td>Metric имеет почти отдельную series на каждую операцию</td><td>В label попал trace id, raw id или свободный текст</td><td>Сверить labels с allow-list и посчитать классы, а не значения</td><td>Убрать identity; оставить низкокардинальный class</td></tr><tr><td>Все отказы помечены одинаково</td><td>Transport failure и domain rejection смешаны в error=true</td><td>Проверить словарь outcome-class</td><td>Разделить transport, domain и retryable processing outcome</td></tr><tr><td>Есть sampling rule, но нет уверенности в покрытии</td><td>План выдаётся за измерение</td><td>Найти реальные данные о rate, collector и retention</td><td>Назвать правило планом; не делать вывод о latency или стоимости</td></tr></tbody></table></div><h2>Почему нельзя смешивать ошибку и результат</h2><p>Transport failure означает, что граница вызова не завершилась ожидаемо. Domain rejection означает, что запрос дошёл до правила и получил допустимый отрицательный исход. Retryable processing failure означает, что worker может повторить работу. Эти состояния требуют разных действий. Одно поле <code>error=true</code> не говорит, искать ли сеть, бизнес-правило или повтор.</p><p>Для учебной схемы достаточно небольшого словаря: <code>transport-unavailable</code>, <code>domain-rejected</code>, <code>retryable-processing</code>, <code>accepted</code>. Не следует добавлять в metric текст исключения. Он может содержать input, URL, имя клиента или случайный идентификатор. Подробный event, если он действительно нужен, должен иметь отдельный канал, доступ и retention. Trace context не является разрешением на хранение payload.</p><h2>Sampling не исправляет плохую схему</h2><p>Sampling выбирает объём trace-наблюдений. Cardinality определяет, сколько отдельных серий создаёт metric. Если metric получила raw user id, правило sampling для trace не уменьшает проблему metric. Если worker context потерян, двадцать процентов сохранённых trace не докажут связь с worker. Поэтому sampling записывают рядом с причиной, границей и ожидаемым вопросом. Формулировка «ошибка или 1 из 20» здесь только учебная. Она не сообщает реальную долю, стоимость хранения или полноту покрытия.</p><p>Время также нельзя складывать без границ. UI wait, server processing, queue delay и worker execution — разные сегменты. Один root span может скрыть ожидание очереди и retry. Три коротких span могут не показать путь, если context оборвался. Пока не определены часы, события и повторные попытки, статья не делает вывода о bottleneck. Это отрицательный путь: отсутствие данных о границе запрещает уверенный performance claim.</p><h2>Порядок действий</h2><ol><li>Выбрать один пользовательский путь и назвать три границы: UI, API, worker.</li><li>Для каждой границы задать главный сигнал и один вопрос, на который он отвечает.</li><li>Назначить технический context и проверить, что он одинаково представлен на каждом переходе.</li><li>Описать message boundary: что переносится, кто создаёт новую операцию и что происходит при пустом context.</li><li>Составить allow-list полей и отдельный список запретов для identity, свободного текста и query.</li><li>Разделить outcome-class для transport, domain и retryable processing.</li><li>Проверить учебным validator-ом положительный hand-off и три отрицательных случая: нет context, запрещённое поле, operational verb вместо plan.</li><li>Только после этого согласовать реальный collector, access, retention, sampling и тест в разрешённой среде.</li></ol><h2>Ограничения</h2><p>Один и тот же context в трёх литералах не доказывает, что заголовок дойдёт через browser, proxy и очередь. SVG не доказывает наличие SDK. Учебный код не измеряет latency, throughput, error rate или стоимость telemetry. OpenTelemetry и W3C задают терминологию и форматы, но не назначают словарь вашей команды, права доступа, срок хранения и правила редактирования данных.</p><p>Нельзя объявлять проблему решённой только потому, что тест снова прошёл. Повторный запуск мог попасть в другую ветку, а retry мог скрыть отказ. Нельзя объявлять metric безопасной только по короткому имени label. Нужны допустимые values и проверка неизвестного значения. Если вопрос требует индивидуального payload, его нельзя протащить в агрегат под видом «диагностики».</p><h2>Проверяемый критерий готовности</h2><p>Материал готов к отдельному design review, когда для одного пути есть карта UI → API → worker, названный владелец каждой границы, один technical context, allow-list полей и словарь outcome-class. Validator должен вернуть только <code>synthetic-observability-plan-hand-off</code> для полного учебного объекта. Для пустого context он обязан вернуть <code>stop-broken-correlation-context</code>; для forbidden field — <code>stop-forbidden-signal-field</code>. Ни один результат не должен называться deploy, rollout или production success.</p><p>После этого готовность системы проверяют уже другими средствами: разрешённым тестом propagation, проверкой редактирования данных, контролем доступа, измерением cardinality и сопоставлением реального trace с запросом. Пока этих доказательств нет, корректный итог — ограниченный hand-off и точный stop, а не красивая легенда о сквозной наблюдаемости.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — различия между traces, metrics и logs.</li><li><a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Trace Context</a> — формат и перенос trace context.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/metrics/data-model/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Metrics Data Model</a> — модель metrics, attributes и exemplars.</li><li><a href=\"https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Semantic Conventions for Messaging Spans</a> — propagation между producer и consumer.</li></ul>"
|
||
}
|