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