Наблюдаемость
traceparent и tracestate: полный формат W3C-заголовка
On-call-инженер видит значение traceparent 99-aaaaaaaa-bbbbbbbb-01 в HTTP-заголовке. Валидно ли оно? Использовать его или сгенерировать свежий трейс? Ответ в спеке, и неправильный выбор тихо ломает trace store backend.
Заголовок traceparent по байтам
Спецификация W3C Trace Context (W3C Recommendation с 2020, Level 2 с 2024) определяет один HTTP-заголовок фиксированной ширины. Значение — ровно 55 байт:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01Четыре поля через дефис:
| Поле | Ширина | Текущее значение | Правила |
|---|---|---|---|
| Version | 2 hex (1 байт) | 00 | ff зарезервирован и запрещён |
| Trace-id | 32 строчных hex (16 байт) | 128-битный random | All-zeros недопустим |
| Parent-id | 16 строчных hex (8 байт) | 64-битный span-id вызывающего | All-zeros недопустим |
| Trace-flags | 2 hex (1 байт) | 01 = sampled | Определён только бит 0 (sampled) |
Ширина намеренно фиксирована, чтобы router или sidecar мог парсить заголовок без regex-движка — просто разбей по дефисам и проверь длины байт. Компактная форма означает ~0.04 KB на HTTP-запрос — измеримо, но ничтожно относительно типичных payload’ов.
| Позиция в строке | Поле | Пример | Смысл |
|---|---|---|---|
| 0–1 | version | 00 | Версия спеки; ff запрещён |
| 3–34 | trace-id | 4bf92f3577b34da6a3ce929d0e0e4736 | 128-битный random; all-zeros недопустим |
| 36–51 | parent-id | 00f067aa0ba902b7 | span-id вызывающего; all-zeros недопустим |
| 53–54 | trace-flags | 01 | Бит 0 = sampled; остальные зарезервированы |
Trace-id обязан быть равномерно случайным
Спека категорична: trace-id должен генерироваться криптостойким случайным 128-битным значением. Он не должен кодировать timestamp, IP или user-id потому что:
- Downstream-инструменты полагаются на равномерное распределение для решений по sampling (consistent hashing опирается на это).
- Встроенная информация утекает операционные данные.
OTel SDK используют secure random платформы: crypto.getRandomValues в браузерах, /dev/urandom на Linux, java.security.SecureRandom на JVM. Генерация одного 128-битного id — sub-microsecond, никогда не измеримая стоимость. При 128-битной случайности вероятность коллизии среди триллиона трейсов порядка 10⁻¹⁵ — практически ноль.
Уникальность trace-id — фундамент: tracing-backend ключит все span’ы по trace-id, склеивает при запросе, и если два независимых запроса делят id — backend тихо смешает их в один бессмысленный трейс.
Валидация и fallback «нет заголовка»
Когда сервис получает запрос без traceparent или с невалидным (неверная version, all-zero trace-id, malformed hex), receiver обязан сгенерировать совершенно новый trace-id и стартовать свежий трейс. Спека явно определяет этот fallback, чтобы один сбойный caller не каскадно ломал трейсы.
Сбои валидации должны логироваться как атрибут span’а (invalid_traceparent_received=true), чтобы трейс появлялся в backend с маркером, позволяя операторам найти плохого upstream-caller. Production-команды трактуют ненулевую частоту invalid_traceparent span’ов как propagation-регресс.
Пример 99-aaaaaaaa-bbbbbbbb-01: версия 99 неизвестна (текущий max — 00); trace-id и span-id неправильной длины (8 и 8 hex-символов, не 32 и 16). Заголовок невалиден — принимающий сервис стартует свежий трейс.
Компаньон tracestate
Где traceparent — универсальный идентификатор, tracestate несёт vendor-специфичный контекст: какой sampler принял решение, какая canary-версия эмитнула, какие дополнительные id backend хочет.
Формат: comma-separated key=value, до 32 членов, ключ и значение до 256 символов каждый. Спека обязывает vendor’ов пропагировать не менее 512 символов кумулятивного tracestate, даже если они не понимают ключи — это сохраняет end-to-end tracing через разнородные vendor-инструменты.
Правила мутации:
- Vendor может добавить новый ключ (prepend в начало списка).
- Vendor может обновить свой ключ (передвинуть в начало).
- Vendor не должен удалять ключи других vendor’ов.
Заголовок — escape hatch для того, что не вмещается в жёсткий traceparent.
Интерфейс propagator
OpenTelemetry обобщает логику заголовков двумя операциями над объектом Context:
inject(context, carrier)— пишет нужные заголовки в исходящий запрос.extract(carrier) -> context— читает их из входящего запроса.
Carrier’ы pluggable (HTTP-заголовки, gRPC metadata, AMQP properties, обычный dict), propagator’ы pluggable (W3C TraceContext, W3C Baggage, B3 single, B3 multi, Jaeger). Дефолтный OTel composite — TraceContext + Baggage. B3 поддерживается для interop с Zipkin-эрой.
Библиотека делает bit-кодирование, валидацию и логику «если входящего нет — сгенерируй», поэтому прикладной код никогда не трогает сырой hex.
- Размер заголовка traceparent
- 55 байт (фиксировано)
- Ширина trace-id
- 128 бит / 32 hex-символа
- Ширина span-id (parent-id)
- 64 бита / 16 hex-символов
- Определённых бит trace-flags
- 1 (sampled-флаг, бит 0)
- Макс. членов tracestate
- 32
- Мин. пропагируемых символов tracestate на vendor
- 512
- Вероятность коллизии trace-id при триллионе трейсов
- ~10⁻¹⁵
- Стоимость на запрос
- ~0.04 KB overhead заголовка
Значение traceparent `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`. Что значит финальный `01`?
HTTP-запрос пришёл в твой сервис с `traceparent: 99-aaaaaaaa-bbbbbbbb-01`. Что сервис делает по W3C-спеке?
Vendor читает заголовок tracestate с ключами двух других vendor'ов. Какая мутация запрещена спекой?
- 01Перечисли четыре поля traceparent по порядку и их назначение.
- 02Почему trace-id обязан быть равномерно случайным и почему он никогда не должен кодировать timestamp или IP?
- 03Что такое интерфейс propagator inject/extract и какую проблему он решает?
W3C-заголовок traceparent несёт четыре поля фиксированной ширины — version, 128-битный trace-id, 64-битный parent-id и trace-flags — в ровно 55 байтах. Фиксированная ширина — намеренный выбор дизайна: любой router или sidecar парсит без regex-движка. Trace-id обязан быть равномерно случайным, никогда не кодировать timestamp’ы или IP, потому что consistent sampler’ы и приватность оба зависят от этого. Невалидный заголовок (неизвестная version, неверная длина поля, all-zero trace-id или parent-id) должен быть отброшен и стартован свежий трейс, залогированный как invalid_traceparent_received для отслеживания регрессий. Компаньон tracestate несёт vendor-специфику — до 32 пар key=value; vendor’ы обязаны пропагировать чужие ключи, не понимая их. OTel propagator interface (inject/extract) оборачивает всё это, чтобы прикладной код никогда не трогал сырой hex.
- Согласованность 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