{"index":7,"slug":"editorial-2027-10-field-long-form-interview","title":"Trace Context в HTTP: как сохранить запрос на границе proxy","excerpt":"Если traceparent исчезает или не проходит проверку, gateway и сервис перестают видеть один запрос. Разбираем контракт proxy, безопасный fallback и проверку через реальный HTTP-маршрут.","contentHtml":"
Симптом появляется во время сбоя: gateway сообщает timeout, backend пишет 500, а записи нельзя связать одним запросом. Инженер видит несколько похожих событий и не понимает, относятся ли они к одной операции. Цена ошибки — лишний поиск по логам, неверная гипотеза о медленном сервисе и более долгий разбор пользовательской проблемы.
Чаще ломается не сама трассировка, а граница между компонентами. Proxy не пропускает заголовок, middleware начинает новый trace вместо продолжения, либо сервис принимает строку с неверным форматом. Главный вопрос статьи — как доказать, что один HTTP-запрос сохранил контекст на этой границе, не превратив trace-id в право доступа или бизнес-идентификатор.
W3C Trace Context задаёт переносимые HTTP-заголовки traceparent и tracestate. Первый несёт общий trace-id, идентификатор родительской операции и flags; второй хранит необязательное состояние конкретных систем трассировки. Контекст связывает участки обработки, но не гарантирует наличие span, корректность логирования или доставку telemetry.
На границе нужно заранее выбрать один из трёх режимов. Обычный proxy пересылает валидный контекст. Инструментированный proxy участвует в трассе, создаёт свой span и меняет parent-id для следующего участка. Защитный gateway может начать новую трассу на доверенной границе. Последний вариант разрывает внешнюю корреляцию по решению безопасности, поэтому его нельзя выдавать за «потерю заголовка».
| Режим | Что уходит дальше | Цена | Когда применять |
|---|---|---|---|
| Forward | Валидные traceparent и tracestate; proxy не создаёт span | Дешевле, но задержка внутри proxy не видна отдельным участком | Тонкий reverse proxy без собственной инструментализации |
| Participate | Тот же trace-id, новый parent-id и span proxy | Нужны SDK, sampling и единые правила имён | Когда задержка маршрутизации входит в SLO сервиса |
| Restart | Новый локальный trace; внешний контекст не становится родителем | Корреляция через границу теряется | Явная trust boundary или защита от злоупотребления входным контекстом |
Для обычной внутренней HTTP-границы начинайте с forward или participate. Владелец proxy должен записать режим в конфигурации и интеграционном тесте. Иначе команда будет спорить по логам, где разные trace-id могут быть как дефектом, так и намеренным restart.
В формате версии 00 значение traceparent содержит четыре поля: version-trace-id-parent-id-trace-flags. Это две hex-цифры версии, 32 lowercase hex-цифры trace-id, 16 lowercase hex-цифр parent-id и две lowercase hex-цифры flags. Trace-id и parent-id не могут состоять из одних нулей. Имя HTTP-заголовка регистронезависимо, но отправлять его следует в lowercase; uppercase внутри значений — уже другая проверка.
Версия 00 — формат, который проверяет пример ниже. Спецификация описывает правила для будущих версий: pass-through-компонент не должен без причины разбирать неизвестное расширение, а инструмент, который участвует в трассе, обязан иметь политику обработки более высокой версии. Поэтому «не поддерживаем version» — это проектное решение парсера, а не универсальное требование W3C.
Извлечение выполняет propagator, а не случайный middleware с split('-'). Невалидный carrier не должен приводить к исключению и не должен записываться в контекст как новый родитель. Если валидного входящего контекста нет, SDK создаёт локальный trace по своей политике. Для валидного удалённого контекста текущий сервис создаёт свой span и передаёт downstream новый parent-id, сохраняя trace-id.
Proxy проверяется отдельно от библиотеки. Сначала выясните, пропускает ли его allow-list заголовок, затем проверьте лимит размера и поведение при нескольких значениях. Нормализация регистра имени — нормальна для HTTP, а удаление поля, непреднамеренный restart или разные правила для HTTP/1.1 и HTTP/2 — уже часть вашего runtime-контракта. Локальный unit test парсера этого не доказывает.
Trace-id не объясняет задержку сам по себе. Для поиска места ожидания нужны границы span, временные отметки и статус ошибки. OpenTelemetry отдельно определяет HTTP span и его атрибуты; если компоненты пишут только общий идентификатор, связь событий появится, но длительность отдельных участков останется неизвестной.
Этот фрагмент можно сохранить как отдельный файл и запустить в Node.js без пакетов. Он проверяет только синтаксический контракт версии 00: не создаёт span, не обращается к collector и не утверждает, что заголовок дошёл через proxy. Причина отказа короткая, поэтому её можно считать в метрике без сохранения полного внешнего значения.
function parseTraceparent(value) { if (typeof value !== 'string') return { ok: false, reason: 'missing-or-not-string' }; const parts = value.split('-'); if (parts.length !== 4) return { ok: false, reason: 'field-count' }; const [version, traceId, parentId, flags] = parts; if (!/^[0-9a-f]{2}$/.test(version) || version === 'ff') return { ok: false, reason: 'version-invalid' }; if (version !== '00') return { ok: false, reason: 'version-unsupported-by-example' }; if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return { ok: false, reason: 'trace-id-invalid' }; if (!/^[0-9a-f]{16}$/.test(parentId) || /^0+$/.test(parentId)) return { ok: false, reason: 'parent-id-invalid' }; if (!/^[0-9a-f]{2}$/.test(flags)) return { ok: false, reason: 'trace-flags-invalid' }; return { ok: true, version, traceId, parentId, flags }; } const fixtures = [['valid', '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'], ['uppercase-value', '00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01'], ['zero-parent', '00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01']]; for (const [name, value] of fixtures) { const result = parseTraceparent(value); console.log(name, result.ok ? 'accepted' : result.reason); } // valid accepted // uppercase-value trace-id-invalid // zero-parent parent-id-invalidUppercase trace-id здесь отклоняется, хотя имя заголовка TraceParent само по себе допустимо. Это controlled fallback, а не HTTP 500: сервис не использует повреждённую строку как родительский контекст. В production коде такую проверку лучше отдать официальному propagator выбранного SDK, а fixture оставить как контракт интеграции и регрессионный тест.
| Симптом | Вероятная граница | Проверка | Действие |
|---|---|---|---|
| В gateway и service разные trace-id | Proxy удалил заголовок, либо выбран restart | Сравнить raw-заголовок до и после proxy и прочитать режим | Исправить allow-list или документировать trust boundary |
| Контекст есть только локально | Runtime применяет другой маршрут, лимит или фильтр | Повторить запрос через тот же gateway с теми же правилами | Добавить integration test gateway + service |
| Валидный на вид заголовок отклонён | Uppercase в значении, длина, нулевой id или version | Прогнать fixture на каждое поле и записать reason | Отбросить вход без 500; не логировать полное значение |
| По trace-id не находится событие | Другой ключ, sampling или задержка доставки | Проверить структурное поле, span и exporter | Развести correlation gap и отсутствие telemetry |
| После HTTP-запроса теряется связь с очередью | HTTP carrier не перенесён в message carrier | Проверить envelope или headers сообщения | Настроить propagator брокера и отдельный link при необходимости |
traceparent до proxy и на входе сервиса. Не передавайте в логах пользовательские данные и не делайте trace-id секретом.ff и выбранную политику для более высокой версии. Для каждого случая зафиксируйте expected decision.Этот подход проверяет HTTP carrier и передачу контекста между компонентами. Он не проверяет подпись, доверие к отправителю, полноту collector, backend sampling или форматы контекста Kafka и другой очереди. Для асинхронного сообщения нужен отдельный message carrier; иногда правильнее использовать span link, а не притворяться прямым parent-child. Повтор операции и бизнес-связь требуют отдельного безопасного operation-id.
Готовность доказана, когда интеграционный тест через реальный proxy подтверждает выбранный режим, валидный context получает ожидаемый локальный span, а повреждённый вход приводит к controlled fallback без 500. Логи используют единый trace-id key, span boundaries позволяют искать задержку, а документация явно говорит, где сделан restart. Если эти условия не проверены, проблема «trace-id пропал» остаётся гипотезой.