Files

8 lines
16 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": 156,
"slug": "editorial-2023-09-practice-telemetry-signals",
"title": "Логи, метрики и трассы: как связать один сбой без взрыва cardinality",
"excerpt": "Пошаговая схема корреляции для распределённого запроса: метрика показывает класс отказа, trace — путь, а структурированный лог — причину конкретного события.",
"contentHtml": "<p>После релиза возникает сбой в авторизации: график показывает рост отказов, но по нему нельзя найти конкретный запрос. В логах сообщения есть, однако они не связаны с трассировкой. Инженер вручную перебирает временной интервал и рискует исправить не ту границу. Лишний retry увеличивает нагрузку, а необоснованный timeout прячет задержку.</p>\n<p>Рабочая схема разделяет три роли. Метрика отвечает, как часто возникает класс событий. Trace, то есть распределённая трасса, показывает путь одного запроса через сервисы. Структурированный лог фиксирует событие и его безопасный контекст. Один trace ID связывает trace и лог, но не должен становиться label метрики: иначе корреляция создаст неконтролируемое число временных рядов.</p>\n<h2>Начните с вопроса расследования</h2>\n<p>До настройки SDK и дашборда запишите, какой факт требуется получить. Для всплеска ошибок авторизации вопрос звучит так: «какие операции и на каких границах отклоняют запросы?». У каждого сигнала будет свой ответ, поэтому один идентификатор нельзя механически разложить по всем полям.</p>\n<table><thead><tr><th>Сигнал</th><th>Вопрос</th><th>Пример поля</th><th>Ограничение</th></tr></thead><tbody><tr><td>Метрика</td><td>Как меняется частота класса?</td><td><code>outcome=rejected</code></td><td>Не указывает запрос</td></tr><tr><td>Trace</td><td>Какие шаги прошёл запрос?</td><td><code>trace_id</code>, <code>span_id</code></td><td>Не сохраняет каждый запрос</td></tr><tr><td>Лог</td><td>Что произошло на шаге?</td><td><code>event_name</code>, <code>reason_class</code></td><td>Не заменяет агрегат</td></tr></tbody></table>\n<p>OpenTelemetry описывает trace как путь запроса, metric как измерение во время работы, а log как запись события. Сигнал выбирают по вопросу, а не по открытому у инженера хранилищу.</p>\n<h2>Опишите контракт на границах сервисов</h2>\n<p>Рассмотрим запрос <code>checkout</code>. Gateway принимает HTTP-запрос, передаёт контекст сервису оплаты, а payment создаёт дочерний span <code>authorize</code>. При отказе payment пишет событие и увеличивает счётчик. Контракт проверяется по пяти переходам:</p>\n<ol><li>На входе gateway прочитать или создать trace context.</li><li>Передать контекст на исходящем вызове в payment.</li><li>Создать дочерний span вокруг авторизации.</li><li>Записать из активного контекста тот же trace ID и span ID фактической причины.</li><li>Увеличить метрику с labels сервиса, нормализованного маршрута и класса результата.</li></ol>\n<p>Для HTTP таким переносом обычно служит заголовок <code>traceparent</code>, определённый W3C Trace Context. Он не является пользовательским request ID и должен проходить проверку формата. На каждой границе сравнивайте trace ID: downstream продолжает тот же trace, а не начинает новый. Если клиент не поддерживает propagation, исправляйте интеграцию, а не добавляйте trace ID в metric.</p>\n<figure><img src=\"/assets/editorial/2023/telemetry-signals-2023-correlation-map.svg\" alt=\"Учебная схема: общий trace ID соединяет gateway и payment spans с отказавшим логом, а метрика содержит только service, route и outcome\"><figcaption>Trace и лог связываются по контексту запроса; метрика сохраняет только класс события и остаётся агрегируемой.</figcaption></figure>\n<h2>Оставьте уникальные значения вне labels</h2>\n<p>В модели Prometheus каждый уникальный набор значений labels создаёт отдельный временной ряд. Если добавить к счётчику <code>trace_id</code>, почти каждый запрос создаст новый ряд. Тот же риск несут <code>user_id</code>, <code>order_id</code>, email и сырой URL с идентификаторами. Backend может принять такие значения, но стоимость хранения и запросов растёт вместе с комбинациями.</p>\n<p>В label оставляйте поля с ограниченным словарём. Вместо <code>/orders/8472</code> используйте <code>/orders/:id</code>; вместо текста исключения — класс <code>limit</code>, <code>invalid_input</code> или <code>upstream_timeout</code>. Список классов — часть контракта и должен быть согласован с владельцем дашборда.</p>\n<table><thead><tr><th>Поле</th><th>Trace или log</th><th>Metric label</th><th>Причина</th></tr></thead><tbody><tr><td><code>trace_id</code></td><td>Да, для перехода к пути</td><td>Нет</td><td>Почти неограниченное множество</td></tr><tr><td><code>route_template</code></td><td>Да</td><td>Да</td><td>Ограниченный словарь</td></tr><tr><td><code>reason_class</code></td><td>Да</td><td>Да, если согласован</td><td>Группирует причины</td></tr><tr><td><code>order_id</code></td><td>Только при разрешённом доступе</td><td>Нет</td><td>Высокая cardinality и чувствительность</td></tr></tbody></table>\n<h2>Проверьте договор на учебных данных</h2>\n<p>Это форма договора, а не готовый OpenTelemetry exporter. Идентификаторы с префиксом <code>demo-</code> вымышлены. Фрагмент проверяет совпадение trace ID в trace и логе и отсутствие уникального поля в metric sample.</p>\n<pre><code>const trace = {\n traceId: 'demo-trace-001',\n spans: [\n { spanId: 'demo-gateway', service: 'gateway', operation: 'checkout' },\n { spanId: 'demo-payment', service: 'payment', operation: 'authorize' },\n ],\n};\nconst logEvent = {\n trace_id: trace.traceId,\n span_id: 'demo-payment',\n event_name: 'payment.authorization.rejected',\n reason_class: 'limit',\n};\nconst metricSample = {\n name: 'payment_authorization_total',\n labels: { service: 'payment', route: 'checkout', outcome: 'rejected' },\n value: 1,\n};\nconsole.assert(logEvent.trace_id === trace.traceId);\nconsole.assert(!Object.hasOwn(metricSample.labels, 'trace_id'));</code></pre>\n<p>Проверки подтверждают только форму объекта. В рабочем сервисе данные должны быть результатом инструментирования и экспортироваться в выбранный backend. Demo-данные не показывают реальную задержку, частоту отказов или полноту sampling.</p>\n<h2>Воспроизведите связь через HTTP</h2>\n<p>Если тестовый gateway слушает <code>localhost:8080</code>, передайте ему фиксированный учебный контекст. Заголовок соответствует формату W3C и предназначен для лабораторной проверки. Замените URL и имя файла на настройки своей среды.</p>\n<pre><code>curl -sS -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' http://localhost:8080/checkout\njq 'select(.trace_id == \"4bf92f3577b34da6a3ce929d0e0e4736\") | {trace_id, span_id, event_name}' app.log</code></pre>\n<p>Сверьте три наблюдения: в trace появился путь gateway → payment; в логе есть тот же <code>trace_id</code> и span оплаты; график показывает серию <code>service=payment, route=checkout, outcome=rejected</code>. HTTP 200 этого не доказывает. Если endpoint не создаёт отказ, используйте тестовый сценарий с известным ответом.</p>\n<h2>Разберите симптом по таблице</h2>\n<table><thead><tr><th>Наблюдение</th><th>Гипотеза</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>График растёт, trace не находится</td><td>Нет перехода к контексту или trace отброшен sampling</td><td>Взять лог отказа и найти его trace ID</td><td>Настроить переход; sampling проверить отдельно</td></tr><tr><td>Новая series появляется почти на каждый запрос</td><td>В label попал ID или сырой URL</td><td>Посчитать значения label за окно</td><td>Удалить уникальное поле и нормализовать маршрут</td></tr><tr><td>Trace общий, span указывает gateway</td><td>Лог пишется вне активного span</td><td>Сопоставить span ID с операцией отказа</td><td>Писать событие внутри нужного контекста</td></tr><tr><td>Payment видит новый trace</td><td>Не сработал propagator</td><td>Сравнить входящий и исходящий <code>traceparent</code></td><td>Исправить middleware или клиент</td></tr></tbody></table>\n<p>Таблица отделяет неисправность контекста от sampling и плохой схемы labels. После исправления повторите тот же тестовый запрос.</p>\n<h2>Проверьте отрицательные сценарии</h2>\n<p>Счастливый путь доказывает лишь то, что корреляция иногда работает. Подмените span ID в логе и убедитесь, что проверка указывает на неверный шаг. Уберите trace context и проверьте, что запись без trace ID не смешивается с другой трассой. Запустите два параллельных запроса: одинаковая временная метка не может быть единственным ключом связи.</p>\n<p>Отдельно проверьте рост словаря labels. В тестовом инструменте добавьте уникальный ID намеренно, посчитайте новые серии, затем удалите его и сравните число рядов с исходным диапазоном. Для этого достаточно изолированного Prometheus-compatible backend; production-трафик не нужен.</p>\n<h2>Зафиксируйте границы применимости</h2>\n<p>Схема не гарантирует trace для каждого отказа. Sampling может сохранить только часть запросов, сборщик — потерять данные, а политика хранения — удалить старые записи. Метрика показывает агрегированный класс, но не полный список причин. Для критичных операций заранее определите sampling и срок хранения.</p>\n<p>Trace ID не является разрешением на доступ к данным. Ссылка из метрики в trace должна учитывать права пользователя. Логи с trace ID всё равно могут содержать персональные данные, токены или платёжные реквизиты; корреляция не отменяет маскирование и ограничение доступа.</p>\n<p>Низкая cardinality не означает низкую стоимость для любого backend. Prometheus описывает labels как измерения временных рядов и предупреждает о high-cardinality values. Для другой системы уточните модель хранения, индексацию и sampling. Имена полей зависят от языка и SDK.</p>\n<h2>Критерий готовности</h2>\n<p>Договор проверен, когда тестовый запрос проходит нужные границы, downstream сохраняет общий trace ID, лог содержит trace ID и span ID фактической причины, а метрика группируется по ограниченным labels. Зафиксируйте также тест потери контекста, проверку cardinality и правило доступа к логам и трассам. Иначе dashboard показывает сигнал, но не даёт воспроизводимого маршрута расследования.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener\">OpenTelemetry: Signals</a> — роли traces, metrics и logs.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/logs/data-model/\" target=\"_blank\" rel=\"noopener\">OpenTelemetry Logs Data Model</a> — поля <code>TraceId</code> и <code>SpanId</code>.</li><li><a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener\">W3C Trace Context</a> — формат HTTP-контекста между компонентами.</li><li><a href=\"https://prometheus.io/docs/concepts/\" target=\"_blank\" rel=\"noopener\">Prometheus: Data model</a> и <a href=\"https://prometheus.io/docs/practices/naming/\" target=\"_blank\" rel=\"noopener\">Metric and label naming</a> — временные ряды и high-cardinality labels.</li></ul>"
}