Files
progcode/editorial/agent-rewrites/051.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 51,
"slug": "editorial-2026-08-practice-end-to-end-observability",
"title": "Сквозная наблюдаемость без ложных связей: UI, API и worker",
"excerpt": "Как сохранить correlation context между UI, API и worker, выбрать отдельный смысл для span, log и metric и остановиться, если связь или граница данных нарушена.",
"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 создаёт span <code>ui.checkout.submit</code>. API пишет структурированный log <code>api.accepted</code>. Worker увеличивает metric <code>worker.jobs</code>. Все три записи получают технический correlation context <code>fixed-trace-7f</code>.</p><p>Context связывает позиции в одной операции. Он не превращает сигналы в один формат и не отвечает на все вопросы сразу. Span показывает ход ограниченной операции и её длительность. Log объясняет одно событие и его outcome-class. Metric считает повторяющиеся события по небольшому словарю классов. Если записать trace id как label метрики, агрегат начнёт хранить идентификаторы отдельных операций. Это уже не полезная группировка.</p><p>У асинхронной границы нужен отдельный контракт. API должен решить, что именно передаётся в сообщение, кто создаёт дочернюю операцию и что делать при отсутствии context. Worker не может считать, что очередь сама сохранила 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 path = [{ component: 'ui', signal: 'span', context: 'fixed-trace-7f', fields: ['route-template', 'request-kind'] }, { component: 'api', signal: 'log', context: 'fixed-trace-7f', fields: ['operation-name', 'outcome-class'] }, { component: 'worker', signal: 'metric', context: 'fixed-trace-7f', fields: ['job-kind', 'outcome-class'] }];\nconst forbidden = ['email', 'raw-user-id', 'free-text-query'];\nconst sameContext = new Set(path.map((step) =&gt; step.context)).size === 1;\nconst safe = path.every((step) =&gt; step.fields.every((field) =&gt; !forbidden.includes(field)));\nif (!sameContext || !safe) return 'stop: review the boundary';\nreturn 'synthetic-observability-plan-hand-off';</code></pre><p>Это учебный JavaScript-пример. Он проверяет заранее заданный объект в памяти. Он не создаёт 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.</li></ul>"
}