Files

8 lines
24 KiB
JSON
Raw Permalink 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": 49,
"slug": "editorial-2026-08-field-end-to-end-observability",
"title": "Сквозная наблюдаемость без ложной связи: UI, API и worker",
"excerpt": "E2E-тест падает, API пишет лог, worker считает задачу, но причина теряется между границами. Разбираем trace context, роли trace, log и metric, проверку очереди и отрицательный путь.",
"contentHtml": "<p>E2E-тест сообщает об ошибке, но в панели нельзя быстро найти соответствующий backend-запрос. В логах есть время и название операции. Worker показывает метрику. UI показывает упавший шаг. Однако эти записи нельзя надёжно связать. Инженер тратит часы на ручное сравнение временных окон и похожих идентификаторов.</p>\n<p>Цена ошибки выше времени расследования. Команда может исправить worker, хотя проблема возникла при передаче контекста из API в очередь. Может включить повтор, хотя первая команда уже была принята. Может добавить в логи email, полный URL или сырой идентификатор пользователя. Тогда диагностика ускорится на один случай, но данные станут чувствительнее, а метрики — бесполезнее из-за высокой кардинальности.</p>\n<p>Тезис статьи простой: end-to-end наблюдаемость начинается с проверяемой связи между границами, а не с количества панелей. Сначала нужно назвать один путь, один технический context и вопрос для каждого сигнала. Затем нужно запретить поля, которые не нужны этому вопросу. Если связь или границу данных нельзя доказать, система должна вернуть точный stop, а не дорисовать причинную историю по похожему имени.</p>\n<h2>Что именно связывает сквозной сигнал</h2>\n<p>Рассмотрим путь <code>ui.checkout.submit</code> → API → worker. UI создаёт операцию и отправляет запрос. API принимает запрос и ставит работу в очередь. Worker получает сообщение и выполняет работу. Это три разные границы. Между ними передаётся не весь объект операции, а небольшой технический контекст, который позволяет понять: записи относятся к одному пути.</p>\n<p>Correlation context не отвечает на вопрос «какой пользователь это сделал». Он отвечает на вопрос «какие сигналы относятся к одной технической цепочке». Это различие важно для доступа и хранения. Идентификатор пользователя, email, текст формы и полный query string не становятся допустимыми только потому, что их удобно искать. Для расследования отдельной операции может потребоваться другой защищённый процесс. Его нельзя незаметно встроить в общий label или trace attribute.</p>\n<p>У trace, log и metric разные задачи. Span показывает последовательность и границы операции. Structured log объясняет решение в конкретной ветке: например, API принял задачу или отклонил её по известному классу. Metric агрегирует повторяющиеся события: число jobs по ограниченному <code>job-kind</code> и <code>outcome-class</code>. Один context может связать сигналы, но не превращает их в один и тот же тип данных.</p>\n<figure><img src=\"/assets/editorial/2026/end-to-end-observability-2026-investigation-evidence-loop.svg\" alt=\"Цикл проверки сквозной наблюдаемости: вопрос, correlation context, граница данных, sampling и точный hand-off или stop\" loading=\"lazy\" /><figcaption>Схема показывает порядок проверки. Сначала формулируется вопрос, затем проверяются связь и состав полей. Цикл заканчивается проверяемым hand-off или точным stop, а не выводом о production-системе.</figcaption></figure>\n<h2>Механизм: переносимый context и локальные роли</h2>\n<p>Слово «correlation» часто скрывает две разные задачи. Trace context строит отношение между операциями распределённой трассировки. Прикладной correlation ID может связывать запись заказа, команду или обращение в поддержку. Они могут жить рядом, но один идентификатор не обязан заменять другой. В этой статье context относится только к технической цепочке <code>ui.checkout.submit</code> → API → worker.</p>\n<p>Для HTTP существует стандартизированный формат W3C Trace Context. В нём <code>traceparent</code> переносит положение запроса в trace-графе, а необязательный <code>tracestate</code> предназначен для данных поставщиков. Это не разрешение пересылать в заголовке пользовательские поля. На каждой границе нужно проверить формат, доверие к входу и список допустимых атрибутов.</p>\n<p>В терминах OpenTelemetry propagator извлекает context из входного carrier и вставляет его в исходящий carrier. Для HTTP carrier обычно представлен заголовками. Для очереди нужен адаптер конкретного транспорта: он должен назвать поле сообщения или headers, точку извлечения и точку вставки. Сам факт наличия SDK не доказывает, что произвольный брокер сохранит родительскую связь.</p>\n<p>После очереди меняется и семантика времени. Принятие команды, ожидание в очереди и выполнение worker — разные участки. Retry может породить несколько попыток одной команды. Поэтому в логах и метриках полезно разделить <code>accepted</code>, <code>processing</code> и <code>completed</code>, а в trace явно обозначить связь с сообщением и попыткой. Нельзя назвать worker «успешным» только потому, что он получил сообщение.</p>\n<h2>Учебный пример: проверка карты сигналов</h2>\n<p>Ниже приведён ограниченный учебный пример. Он не обращается к браузеру, API, очереди или telemetry backend. Он не доказывает, что в production есть нужный context. Его задача — показать fail-closed правило: validator принимает только три named signals с одним context и отклоняет запрещённое поле.</p>\n<pre><code>const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';\n\nfunction injectTraceparent(context, carrier) {\n if (!context?.traceparent) throw new Error('missing context');\n carrier.traceparent = context.traceparent;\n}\n\nfunction extractTraceparent(carrier) {\n const value = carrier?.traceparent;\n const validShape = /^\\w{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/.test(value || '');\n return validShape ? { traceparent: value } : null;\n}\n\nconst httpHeaders = {};\nconst messageHeaders = {};\ninjectTraceparent({ traceparent }, httpHeaders);\nconst apiContext = extractTraceparent(httpHeaders);\nif (!apiContext) throw new Error('stop: invalid HTTP context');\ninjectTraceparent(apiContext, messageHeaders);\n\nconst workerContext = extractTraceparent(messageHeaders);\nconsole.log(workerContext?.traceparent === traceparent); // true\nconsole.log(extractTraceparent({ traceparent: 'trace-7f' })); // null</code></pre>\n<p>Положительный результат означает только то, что учебный carrier сохранил строку и worker смог проверить её форму. Отрицательный результат для <code>trace-7f</code> показывает нужное поведение при повреждённом значении. Код не создаёт span, не отправляет запрос и не доказывает работу конкретной библиотеки.</p>\n<p>В интеграционном тесте замените обычные объекты реальным HTTP-клиентом и тестовым message broker. Проверьте значение на входе API, значение в опубликованном сообщении и значение после извлечения worker. Отдельно проверьте, что сообщение без context не получает новый идентификатор «для удобства».</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика разрыва сквозной наблюдаемости</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>E2E-тест и API-лог нельзя связать</td><td>UI не передал технический context или лог потерял его</td><td>Сверить context на входе API и в span операции</td><td>Назвать carrier и остановить cross-boundary вывод при его отсутствии</td></tr><tr><td>API и worker выглядят связанными по времени, но причина спорная</td><td>Связь построена эвристикой по timestamp или job name</td><td>Проверить точное значение context и parent/message boundary</td><td>Убрать эвристику; вернуть stop для неподтверждённой цепочки</td></tr><tr><td>Metric содержит trace id или user id</td><td>Индивидуальный идентификатор стал label</td><td>Посчитать уникальные значения и проверить список attributes</td><td>Оставить low-cardinality class; индивидуальный след вынести в отдельный доступный канал</td></tr><tr><td>Лог помогает одному расследованию, но быстро растёт</td><td>В log попал свободный payload или полный текст ошибки</td><td>Проверить schema, размер записи и наличие query, email, телефона</td><td>Оставить operation name и outcome class; payload удалить или ограничить policy</td></tr><tr><td>После sampling «всё равно» видны чувствительные поля</td><td>Sampling перепутали с разрешением на сбор</td><td>Разделить sampling rule и data allow-list</td><td>Сначала убрать запрещённое поле, затем отдельно обсуждать объём traces</td></tr><tr><td>Worker показывает успешную metric, а пользователь получил ошибку</td><td>Metric измеряет получение job, а не итог операции</td><td>Сверить смысл outcome-class и место инкремента</td><td>Развести accepted, processing и completed; не называть одно другим</td></tr></tbody></table></div>\n<h2>Почему sampling не решает cardinality</h2>\n<p>Cardinality описывает число разных комбинаций значений в измерении. Sampling выбирает, какие события или traces сохранять. Это разные решения. Если label содержит email, сохранение одного из двадцати событий не делает поле low-cardinality и не меняет его смысл. Если <code>outcome-class</code> имеет небольшой закрытый словарь, ему не нужен trace id в качестве дополнительного измерения.</p>\n<p>Сначала определите вопрос метрики. Для worker это может быть количество jobs по классу работы и ограниченному исходу. <code>job-kind</code> должен приходить из закрытой taxonomy. Новое значение должно пройти изменение схемы, а не появиться из свободного текста сообщения. Не используйте текст исключения, полный URL, request id или сырые идентификаторы как metric dimension.</p>\n<p>Sampling тоже требует причины и границы. Например, правило может отдельно обсуждать ошибки и обычный путь. Но статья не может назвать coverage, стоимость или процент потерь без реального измерения. Учебное правило — это только параметр дизайна. Оно не доказывает, что выбранный объём достаточен для SLA или расследования.</p>\n<h2>Очередь, retry и состояние операции</h2>\n<p>Асинхронная граница ломается не только из-за потерянного заголовка. Сообщение может быть принято брокером, доставлено дважды, обработано после задержки или завершиться ошибкой после записи результата. Один trace ID не отвечает на все эти вопросы. Добавьте в технический контекст идентификатор операции и номер попытки только тогда, когда это разрешено контрактом; не путайте их с пользовательскими данными.</p>\n<p>Для каждой команды полезно зафиксировать переходы: <code>accepted</code> — API принял запрос и создал сообщение; <code>processing</code> — worker начал попытку; <code>completed</code> — бизнес-операция завершилась по определённому результату. Повторная доставка должна иметь понятное поведение. Идемпотентный ключ может защитить эффект от дубля, но его формат, срок хранения и границы действия зависят от конкретного хранилища.</p>\n<p>Проверяйте не только счастливый путь. Нужны случаи: API не смог опубликовать сообщение, сообщение пришло без context, worker упал после начала обработки, retry получил старую версию данных, а UI потерял ответ после успешного завершения. Для каждого случая назовите локальный сигнал, ожидаемый статус и допустимость сквозного вывода. Если доказательства не хватает, результат должен быть «связь не подтверждена».</p>\n<h2>Порядок действий</h2>\n<ol><li>Запишите наблюдаемый симптом и цену неверного вывода: потеря времени, повтор операции, лишние данные или неправильный fix.</li><li>Сузьте сценарий до одного пути, например <code>ui.checkout.submit</code> → API → worker.</li><li>Для UI, API и worker назовите главный signal и вопрос, на который он отвечает.</li><li>Опишите technical correlation context, carrier, точки extraction и injection.</li><li>Зафиксируйте поведение при пустом, неверном или отсутствующем context.</li><li>Составьте allow-list полей и отдельно forbidden-list: email, phone, raw user id, свободный payload и URL с query.</li><li>Разведите accepted, processing и completed, если путь содержит очередь или retry.</li><li>Проверьте cardinality до обсуждения sampling. Для metric оставьте только закрытые классы.</li><li>Проверьте отрицательные варианты: worker без context, запрещённое поле и неизвестный outcome.</li><li>Добавьте интеграционный тест на реальную границу сообщения и отдельный тест на безопасную схему сигналов.</li><li>Передайте результат как карту вопроса, границ, полей и stop. Не называйте её incident report или production evidence без соответствующих данных.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Описанный механизм не доказывает, что конкретный SDK корректно переносит context через браузер, HTTP-клиент или очередь. Необходимы тесты с реальным carrier и версиями библиотек. Стандарт задаёт формат и семантику контекста, но не выбирает права доступа, retention, список разрешённых бизнес-полей или способ обработки customer data.</p>\n<p>Механизм также не отвечает на вопрос, действительно ли пользователь увидел результат. Наличие span до worker не доказывает доставку UI-ответа. Metric <code>worker.jobs</code> не доказывает завершение операции. Для этого нужны отдельные сигналы и договорённость о состоянии команды. Не смешивайте техническую связь с бизнес-подтверждением.</p>\n<p>Если context потерян, не восстанавливайте его по времени, имени операции или ближайшей записи. Сохраните локальный сигнал, обозначьте границу и верните stop. Если schema неизвестна, не принимайте свободный JSON как допустимый payload. Если outcome не входит в закрытый словарь, классифицируйте его как <code>unknown</code> и передайте владельцу taxonomy. Такой результат выглядит менее удобным, но его можно проверить.</p>\n<p>Статья не описывает production-исследование, не сообщает latency, error rate, sampling coverage или экономию времени. Все значения в коде и примере учебные. Их можно использовать как форму проверки границ, но нельзя цитировать как результат запуска.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Сценарий готов к следующему техническому review, если другой инженер без устного объяснения может ответить на четыре вопроса: какой путь проверяется, какой context связывает границы, какие поля разрешены и что произойдёт при нарушении. Проверка должна показать один named context на UI, API и worker; раздельную семантику span, log и metric; отсутствие запрещённых полей; закрытый словарь outcome-class; и точный stop для разрыва связи.</p>\n<p>Для реальной системы добавьте доказательство транспорта: интеграционный тест передаёт context через HTTP и message boundary, worker сохраняет ожидаемую связь, а неизвестный или пустой input не получает искусственный идентификатор. Отдельно проверьте, что metric не принимает trace id и user id как labels. Пока эти проверки не пройдены, готова только схема расследования, а не end-to-end наблюдаемость.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальные определения traces, metrics, logs и baggage и их различающихся ролей.</li><li><a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Trace Context</a> — нормативный формат HTTP-заголовков <code>traceparent</code> и <code>tracestate</code> и модель распространения контекста между сервисами.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/context/api-propagators/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Propagators API</a> — требования к extract/inject через carrier и поддержке W3C Trace Context.</li><li><a href=\"https://prometheus.io/docs/practices/naming/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Metric and label naming</a> — официальное предупреждение о высокой cardinality и user ID/email в labels.</li></ul>"
}