awesome-everything EN
↑ Обратно к восхождению

Наблюдаемость

Propagation trace-контекста в логах

Суть Каждая log-строка, эмитированная в traced-запросе, обязана нести trace_id и span_id. Без них пивот log-to-trace ломается и observability деградирует до угадывания.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 12 min

Инженер находит log-строку с 5xx ошибкой от checkout-сервиса. Ищет контекст. У log-строки нет trace_id. Она не может открыть трейс, не видит, какой downstream-вызов упал, и не может восстановить request flow. У неё симптом — и больше ничего. Без trace_id структурные логи — остров.

Почему trace_id — самое ценное поле

Log-строка с trace_id кликабельна в соответствующий distributed trace. Этот трейс показывает каждый span каждого сервиса, тронувшего запрос: per-span timing, dependency-вызовы, error-span’ы, upstream/downstream-контекст. Log-строка больше не остров — это узел в полном observability-графе.

Без trace_id та же log-строка несёт симптом, но не путь к причине. Единственный вариант — коррелировать по timestamp в надежде, что никакие другие запросы не перекрывались — что ломается в тот момент, как сервис обрабатывает более одного concurrent-запроса.

OTel Logs Data Model кодирует это отношение напрямую: каждая log-запись несёт TraceId, SpanId и TraceFlags как first-class поля, унаследованные от активного span’а на emit-time. Это не опциональные метаданные — это структурный join-ключ.

Без trace_idС trace_id
Log-строка — остров. Знаешь, что что-то случилось — не знаешь почему.Log-строка ссылается на полный distributed trace запроса.
Корреляция требует timestamp-угадывания между сервисами.Один клик открывает span’ы всех задействованных сервисов.
Concurrent-запросы перемежаются в результатах log-поиска.Запрос по trace_id изолирует ровно один запрос end-to-end.
Расследование инцидента: минуты на поиск root service.Расследование инцидента: один пивот к failing span.

Как trace-контекст попадает в log-строку

Trace-контекст — immutable struct, хранящий текущий trace_id, span_id и trace flags — живёт в language-scoped execution context запроса. Logger читает из него на emit-time, автоматически.

  • Node.js: OTel SDK хранит контекст в AsyncLocalStorage. Callback mixin pino (или механизм context.with() из @opentelemetry/api) читает активный span из store на emit-time. Каждый лог, эмитированный в request handler’е, наследует trace_id без per-call-site boilerplate.
  • Go: context.Context несёт активный span. Slog-handler или zap-logger читает из context, переданного в handler-функцию. В отличие от Node, контекст явный — ctx должен пробрасываться по цепочке вызовов.
  • Python: contextvars.ContextVar хранит активный span (Python 3.7+). Processor merge_contextvars structlog читает активный span на render-time.
  • JVM: OTel Java agent auto-инструментирует thread-local context. ThreadContext log4j2 или MDC Logback несут trace_id автоматически, когда agent подключён.

В каждом случае паттерн один: у logger’а есть hook (mixin, processor, handler), консультирующий активный execution context и инжектирующий trace_id и span_id в log-запись без необходимости передавать их явно в вызывающем коде.

Почему это работает

Причина, по которой trace_id auto-injection важен — объём. Типичный сервис эмитит тысячи log-строк в секунду из десятков call-site’ов. Требовать от инженеров вручную передавать {trace_id, span_id} в каждый log-вызов непрактично — это забывается при рефакторинге, пропускается в сторонних библиотеках и игнорируется под давлением времени. Auto-injection на уровне logger’а — единственный надёжный механизм в масштабе.

Pitfalls async-context propagation

Logger читает контекст из активного execution context. Всё, что ломает propagation execution context, ломает trace_id в log-строке. Это самый частый источник записей trace_id = "00000000000000000000000000000000".

Node.js pitfalls: setTimeout, setImmediate, fire-and-forget Promise’ы и worker-thread’ы могут уронить AsyncLocalStorage, если не обёрнуты в context.bind() или context.with(). Callback, запланированный через setTimeout(() => { logger.warn(...) }, 0), срабатывает вне исходного request-контекста, если span не пропагирован явно в callback.

Python pitfalls: asyncio.create_task пропагирует contextvars корректно с Python 3.7. Но thread’ы через threading.Thread и subprocess’ы ProcessPoolExecutor не наследуют контекст автоматически — контекст должен быть захвачен и восстановлен вручную.

Go pitfalls: в Go нет ambient context propagation. context.Context должен пробрасываться явно через каждую функцию в цепочке вызовов, включая goroutine’ы. Goroutine, запущенная без parent-контекста (go func() { logger.Warn(...) }()), не несёт trace_id, если контекст не передан явно.

Обнаружение: запрашивай log-backend на долю trace_id IS NULL или trace_id = "000000000000..." от общего объёма. Если доля выше 1%, что-то роняет контекст на hot-path. Дополни CI-тестом, упражняющим async-пути (делает запрос, ждёт завершения async-callback, assert’ует, что его log-строка несёт inbound trace_id).

Викторина

Node.js-сервис наблюдает, что примерно 5% log-строк имеют trace_id = '00000000000000000000000000000000'. Наиболее вероятная причина?

Викторина

Почему ручная передача trace_id в каждый log-вызов — ненадёжное решение в масштабе?

Расставь шаги по порядку

Упорядочь шаги расследования 5xx ошибки с использованием log-to-trace пивота:

  1. 1 Запрос логов: service=checkout level=error за последние 15 минут
  2. 2 Найти подходящую log-строку; скопировать её поле trace_id
  3. 3 Открыть tracing-backend; вставить trace_id для загрузки полного distributed trace
  4. 4 Определить failing span — с error=true или наибольшей длительностью
  5. 5 Перейти к сервису, владеющему failing span'ом
  6. 6 Запросить логи этого сервиса, отфильтрованные по тому же trace_id, чтобы увидеть его полный log-контекст
  7. 7 Cross-reference: log-message подтверждает тип ошибки; span подтверждает timing и call chain
Вспомните перед уходом
  1. 01
    Как работает trace-context auto-injection в Node.js с pino и OTel SDK?
  2. 02
    В Go почему context propagation не работает автоматически для goroutine'ов и каков fix?
  3. 03
    Каков production-сигнал для регрессий async context propagation и как настроить обнаружение?
Итог

trace_id и span_id — join-ключи, соединяющие log-строку с полным distributed trace. Без них log-строка — остров: есть симптом, нет пути к причине. Механизм: OTel SDK хранит активный span в language-scoped execution context запроса (AsyncLocalStorage в Node, context.Context в Go, contextvars в Python, thread-local MDC в JVM); logger читает из него на emit-time через mixin, processor или handler hook, инжектируя trace_id и span_id автоматически. Failure mode: async-границы (setTimeout, unbound Promise’ы, goroutine’ы без явного контекста, Python thread’ы) роняют execution context, производя all-zeros trace ID. Обнаружение: мониторить долю trace_id IS NULL per service; алерт выше 1%; добавить CI-тест, assert’ующий, что async log-строки несут inbound trace_id.

Связанные уроки
встречается в167
Продолжить восхождение ↑OTel Logs Data Model и audit-логи как подсистема
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.