8 lines
24 KiB
JSON
8 lines
24 KiB
JSON
{
|
||
"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>"
|
||
}
|