Files
progcode/editorial/agent-rewrites/049.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
22 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": 49,
"slug": "editorial-2026-08-field-end-to-end-observability",
"title": "Сквозная наблюдаемость без ложной связи: как довести сигнал от UI до worker",
"excerpt": "E2E-тест падает, API пишет лог, worker считает задачу, но причина теряется между границами. Разбираем correlation 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>Начните с одного технического значения, например <code>trace-7f</code> в учебной модели. UI, API и worker должны явно показать это значение в своей записи. В настоящей системе формат и перенос определяет конкретный контракт, например W3C Trace Context. Важно не название стандарта, а инвариант: каждая граница либо несёт допустимый context, либо end-to-end вывод прекращается.</p>\n<p>API не должен искать «ближайший» trace по времени. Worker не должен присоединяться к trace только потому, что совпал <code>job-kind</code>. Такие эвристики создают убедительную, но недоказанную историю. При пропавшем или некорректном context нужно сохранить локальный сигнал и отдельно отметить, что сквозная связь не подтверждена.</p>\n<p>Асинхронная очередь добавляет смысловую границу. Принятие задачи и её выполнение не являются одной операцией по умолчанию. Для них нужно описать carrier, место извлечения, место вставки и поведение при ошибке. Нельзя считать, что SDK автоматически сохранит родительскую связь через любую очередь. Это должно следовать из контракта message boundary и проверки конкретной реализации.</p>\n<p>Время требует такой же аккуратности. UI waiting, время обработки API, задержка очереди и worker execution — разные интервалы. Их нельзя складывать без источника времени, правил для retry и определения начала и конца каждого участка. Один root span может скрыть задержку очереди. Три коротких span могут скрыть потерянную связь. Поэтому сначала фиксируют границы и допустимый вопрос, а измерение добавляют после этого.</p>\n<h2>Учебный пример: проверка карты сигналов</h2>\n<p>Ниже приведён ограниченный учебный пример. Он не обращается к браузеру, API, очереди или telemetry backend. Он не доказывает, что в production есть нужный context. Его задача — показать fail-closed правило: validator принимает только три named signals с одним context и отклоняет запрещённое поле.</p>\n<pre><code>type Signal = {\n component: 'ui' | 'api' | 'worker';\n name: string;\n context: string;\n fields: string[];\n};\n\nfunction checkMap(signals: Signal[], forbidden: Set&lt;string&gt;) {\n const contexts = new Set(signals.map((signal) =&gt; signal.context));\n const forbiddenFields = signals\n .flatMap((signal) =&gt; signal.fields)\n .filter((field) =&gt; forbidden.has(field));\n\n if (signals.length !== 3 || contexts.size !== 1 || signals.some((s) =&gt; !s.context)) {\n return { status: 'stop-broken-correlation-context' };\n }\n\n if (forbiddenFields.length &gt; 0) {\n return {\n status: 'stop-forbidden-signal-field',\n fields: [...new Set(forbiddenFields)],\n };\n }\n\n return { status: 'synthetic-map-ready-for-review' };\n}\n\nconst result = checkMap([\n { component: 'ui', name: 'span: ui.checkout.submit', context: 'trace-7f', fields: ['route-template'] },\n { component: 'api', name: 'log: api.accepted', context: 'trace-7f', fields: ['outcome-class'] },\n { component: 'worker', name: 'metric: worker.jobs', context: 'trace-7f', fields: ['job-kind'] },\n], new Set(['email', 'raw-user-id', 'request-url-with-query']));\n\nconsole.log(result.status);\n// synthetic-map-ready-for-review</code></pre>\n<p>Этот код проверяет структуру входного объекта в памяти. Он не создаёт trace header и не отправляет данные. Если у worker поставить пустой context, результат станет <code>stop-broken-correlation-context</code>. Если в UI добавить <code>email</code>, результат станет <code>stop-forbidden-signal-field</code>. Это полезный отрицательный путь: отсутствие доказательства не превращается в успешную связь.</p>\n<p>В реальной системе такой validator не заменяет SDK, интеграционный тест, контроль доступа и проверку схемы сообщений. Он задаёт только минимальное правило, которое можно проверять отдельно от транспорта. Полезность примера ограничена именно этим.</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>Порядок действий</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 и их ролей как типов telemetry signals.</li><li><a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Trace Context</a> — официальная рекомендация о переносе trace context и связанных полях traceparent/tracestate.</li></ul>"
}