Перейти к основному содержимому

Проброс контекста

Логгер автоматически добавляет в каждый LogRecord записи W3C Trace Context и W3C Baggage. Корреляция трассировок и логов работает «из коробки»: когда бэкенд группирует записи по trace_id, он видит каждую строку лога, отправленную внутри соответствующего span'а.

Что добавляется

При каждом emit логгер читает активный контекст OpenTelemetry и заполняет:

ПолеИсточникКодирование на wire
trace_idOTel ContextSpanContext.TraceId16 байт (lowercase 32-hex в JSON)
span_idOTel ContextSpanContext.SpanId8 байт (lowercase 16-hex в JSON)
trace_flagsOTel ContextSpanContext.TraceFlagsuint8 (бит sampled и т.п.)
attributes["tenant.id"]W3C Baggage → запись tenant.idунаследованная строка
attributes["request.id"]W3C Baggage → запись request.idунаследованная строка
attributes["user.id"]W3C Baggage → запись user.idунаследованная строка

Другие записи Baggage (реестр будет жить в _meta/baggage_keys.yaml в v1.1 — v1.0-биндинги поставляют список ключей инлайн) тоже добавляются. Если запись отправляется вне любого активного контекста, trace_id и span_id остаются None, а не нулями — бэкенды отличают «нет трассировки» от «трассировка с нулевыми id».

Как это работает

Биндинг Python использует Context API OTel напрямую: opentelemetry.context.get_current() плюс opentelemetry.trace.get_current_span(). Биндинг TypeScript использует trace.getActiveSpan() плюс propagation.getActiveBaggage() из @opentelemetry/api. Биндинг Go читает явный аргумент context.Context, передаваемый в *Ctx-методы уровней, и вызывает oteltrace.SpanFromContext(ctx) из go.opentelemetry.io/otel/trace.

Каждый биндинг работает с одним и тем же источником — параллельной абстракции «dagstack-контекст» нет. Если ты инструментируешь приложение OTel-span'ами (через OpenTelemetry SDK в Python, @opentelemetry/sdk-node в TS, go.opentelemetry.io/otel в Go), проброс trace context работает сам.

Установка записей baggage

W3C Baggage — стандартный для OTel способ пробрасывать сквозные атрибуты через границы сервисов. Установка tenant.id один раз в точке входа запроса делает так, что каждая нижестоящая строка лога несёт это значение, в том числе через HTTP-вызовы (когда подключены OTel HTTP-инструментации).

from opentelemetry import baggage, context

ctx = baggage.set_baggage("tenant.id", "acme-corp")
token = context.attach(ctx)
try:
logger.info("processing request")
# Отправленная запись несёт attributes={"tenant.id": "acme-corp", ...}
# плюс trace_id / span_id из активного span'а (если он есть).
finally:
context.detach(token)

Отказ от проброса

Для fire-and-forget отправок, которым не нужно подтягивать контекст вызывающего кода (редкий случай — обычно это heartbeat-метрика или audit-запись, которая не должна тащить активный tenant), спека определяет opt-out:

logger.withoutContext().info(...) — возвращает дочерний логгер, который пропускает добавление trace и baggage. Opt-out не наследуется дальше; потомки возвращённого логгера снова включают проброс по умолчанию.

Биндинг Python реализует этот opt-out как часть семейства with_sinks / scoped-override в Phase 2. В Phase 1 обходной путь — задавать только явные атрибуты и принимать тот факт, что активный trace context всё равно будет прочитан; если код отрабатывает вне любого span'а, trace_id не добавится.

А что с явным атрибутом по имени trace_id

Спека резервирует trace_id, span_id, trace_flags как типизированные top-level поля LogRecord, а не как ключи атрибутов. Если приложение задаёт attributes={"trace_id": "..."} вручную, это значение попадает в attributes (отдельный слот) и не перетирает field-level trace_id. Бэкенды различают их по структурному местоположению: типизированное поле — для корреляции OTel; атрибут — это данные приложения.

См. также