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

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

Baggage и async-границы: перенос контекста через очереди и callback''''и

Суть baggage пропагирует key-value рядом с трейсом, но неограниченный размер и semi-public видимость делают его PII-ловушкой. Async-границы — setTimeout, Kafka, workers — тихо теряют контекст без явного context.with() или inject/extract.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Разработчик пишет setTimeout(() => doWork(), 100) внутри request-handler’а. Отложенная работа выполняется нормально, эмитирует span’ы и появляется в tracing-дашборде — как совершенно отдельный трейс без связи с запросом, который её запустил.

Baggage: произвольные key-value через journey

Baggage — второй заголовок (baggage), определённый W3C, пропагируется идентично traceparent. Несёт произвольные пары: tenant-id, region, выбор feature-flag, A/B-cohort. Baggage читается downstream-сервисами и может прикрепляться как атрибуты span’а или использоваться кодом (например, для per-tenant квот).

Подвох: baggage по спеке не ограничен, ограничен только carrier-протоколом. HTTP-лимиты заголовка (обычно 8–64 KB на поле, настройка сервера) — практический cap.

Два реальных риска:

  1. Раздутая latency. Baggage пропагируется на каждом hop запроса, каждом сообщении очереди, каждой async-границе. Каждый hop платит дважды: сериализация на исходящем, парсинг на входящем. 16-KB baggage при 10k req/s внутреннего трафика — примерно 160 MB/s чистого baggage-транспорта плюс парсинг на каждом hop.

  2. Утечка PII. Baggage течёт на каждый downstream, включая third-party-интеграции и SaaS API, которые могут логировать или сохранять заголовки — часто в менее аудитируемых контекстах, чем исходная БД. Всё в baggage фактически semi-public внутри и за пределами инженерии.

Дисциплина: baggage хранит операционные теги, никогда — данные идентичности.

Что класть в baggage: tenant-id (opaque-токен), region, состояние feature-flag, имя A/B-cohort, метка request-source. Что никогда не класть: email пользователей, токены карт или платежей, API-ключи, auth-токены, customer PII, внутренние секреты.

Безопасно в baggageНикогда в baggage
tenant-id (opaque-токен)email или полный user-id
region / availability-zoneтокен карты или платежа
состояние feature-flag (например checkout-v2=true)API-ключи или auth-токены
имя A/B-cohortлюбые customer PII

Async-границы: убийцы

HTTP propagation — лёгкий случай: OTel auto-instrumentation обрабатывает его для поддерживаемых HTTP-серверов и клиентов. Тяжёлые — очереди, таймеры, callback’и.

Ловушка setTimeout в Node.js. In-process «текущий контекст» в Node.js течёт через call stack автоматически, но setTimeout, setImmediate и queueMicrotask планируют работу на будущее в другом call stack. Когда callback срабатывает — контекст оригинального запроса исчез. OTel создаёт новый root span (свежий trace-id) для отложенной работы. Результат: трейс, обрывающийся на вызове setTimeout, и отдельный orphan-трейс для отложенной работы.

Фикс: оберни callback в context.bind(ctx, fn) или запусти внутри context.with(ctx, fn):

// BROKEN: traceparent потерян через setTimeout
app.post('/checkout', async (req, res) => {
  setTimeout(() => {
    doDeferredWork(); // новый трейс; не связан с запросом
  }, 100);
  res.status(202).end();
});

// FIXED: привязываем контекст через границу
app.post('/checkout', async (req, res) => {
  const ctx = context.active();
  setTimeout(context.bind(ctx, () => {
    doDeferredWork(); // теперь видит trace-id запроса
  }), 100);
  res.status(202).end();
});

context.bind(ctx, fn) возвращает обёрнутую функцию, восстанавливающую захваченный контекст при вызове. Тот же паттерн для setImmediate, queueMicrotask, callback’ов в third-party-библиотеках и любых custom thread или worker абстракций.

Kafka и propagation через message queue

Когда работа пересекает process-границу через message queue (Kafka, RabbitMQ, SQS, Cloud Pub/Sub) — in-process контекст не может перенестись. Trace-id нужно записать в wire-формат сообщения и прочитать обратно с другой стороны:

  • Сторона producer: вызови propagator.inject(context, message.headers) перед публикацией. Это записывает traceparentbaggage) в map заголовков сообщения.
  • Сторона consumer: вызови context = propagator.extract(message.headers) после polling. Это восстанавливает trace-id, позволяя consumer’у создать дочерний span, связанный со span’ом producer’а.

OTel Kafka instrumentation обрабатывает обе стороны автоматически — но только если instrumentation-библиотека загружена и настроена.

Ключевое различие: context.bind держит in-process контекст живым через async-границу у producer’а, но не записывает traceparent в сообщение. Kafka живёт в отдельном процессе. У consumer’а нет общей памяти с producer’ом; единственный способ нести trace-id — inject его в заголовки сообщения перед публикацией.

Почини сломанную setTimeout-propagation в Node.js

1/3
Викторина

Сервис помещает email пользователя в заголовок baggage для передачи downstream-сервисам. Какой риск?

Викторина

Node.js-сервис использует OTel auto-instrumentation и Kafka. Трейсы от Kafka-consumer'ов — разрозненные orphan'ы. Самая вероятная причина?

Вспомните перед уходом
  1. 01
    Объясни, почему дисциплина по размеру baggage важна в production и что никогда не должно попадать в baggage.
  2. 02
    В чём разница между context.bind (in-process) и inject/extract (cross-process) для trace propagation?
  3. 03
    Назови четыре async-границы в Node.js, требующие явной context propagation, и объясни фикс для каждой.
Итог

W3C-заголовок baggage пропагирует операционные key-value (tenant-id, feature flags, A/B-cohort) рядом с трейсом, но каждый hop платит сериализацию и парсинг, а заголовок течёт на каждый downstream, включая third-party-интеграции. PII или секреты в baggage semi-public внутри и за пределами инженерии. Async-границы — главная причина split-трейсов в production: setTimeout, setImmediate и queueMicrotask срабатывают в другом call stack без OTel-контекста; фикс — context.bind или context.with. Consumer’ы очередей (Kafka, RabbitMQ, SQS) пересекают process-границу — фикс: propagator.inject() в заголовки сообщения у producer и propagator.extract() у consumer; context.bind одного недостаточно.

Связанные уроки
встречается в40
Продолжить восхождение ↑Head sampling и tail sampling: кто решает, какие трейсы выживают
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.