Наблюдаемость
Baggage и async-границы: перенос контекста через очереди и callback''''и
Разработчик пишет 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.
Два реальных риска:
-
Раздутая latency. Baggage пропагируется на каждом hop запроса, каждом сообщении очереди, каждой async-границе. Каждый hop платит дважды: сериализация на исходящем, парсинг на входящем. 16-KB baggage при 10k req/s внутреннего трафика — примерно 160 MB/s чистого baggage-транспорта плюс парсинг на каждом hop.
-
Утечка 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)перед публикацией. Это записываетtraceparent(иbaggage) в 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'ы. Самая вероятная причина?
- 01Объясни, почему дисциплина по размеру baggage важна в production и что никогда не должно попадать в baggage.
- 02В чём разница между context.bind (in-process) и inject/extract (cross-process) для trace propagation?
- 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 одного недостаточно.
- Согласованность sampling и tier tail-sampling Collectorsenior
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Trace propagation: сшей сломанную систему в один tracesenior
- Trace propagation: тест с множественным выборомsenior
- Trace propagation: чтение кода и заголовковsenior
- Trace propagation: тест на свободное воспроизведениеsenior
встречается в40
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Hidden classes, деревья переходов и расположение в памятиmiddle
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Что такое воркеры и зачем они нужныjunior
- Механика web workers: dedicated, shared и OffscreenCanvasmiddle
- Structured clone и transferablesmiddle
- SharedArrayBuffer, Atomics и cross-origin isolationsenior
- Пулы воркеров, Comlink и наблюдаемость в продакшенеsenior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- JSONB, массивы и когда side table побеждаетmiddle
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Где происходит data fetching — и почему это решает LCPjunior
- React Server Components и Suspense streamingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Что делает TLS и зачем он нуженjunior
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior