{"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 может начать новую трассу на доверенной границе. Последний вариант разрывает внешнюю корреляцию по решению безопасности, поэтому его нельзя выдавать за «потерю заголовка».

Выбор режима: что именно делает proxy

РежимЧто уходит дальшеЦенаКогда применять
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.

Механизм: формат, извлечение и новый span

В формате версии 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 и его атрибуты; если компоненты пишут только общий идентификатор, связь событий появится, но длительность отдельных участков останется неизвестной.

\"Клиент
Сначала проверьте режим proxy, затем формат и создание локального span. Заголовок связывает события, но не является авторизацией.

Самодостаточный пример проверки version 00

Этот фрагмент можно сохранить как отдельный файл и запустить в 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-invalid

Uppercase trace-id здесь отклоняется, хотя имя заголовка TraceParent само по себе допустимо. Это controlled fallback, а не HTTP 500: сервис не использует повреждённую строку как родительский контекст. В production коде такую проверку лучше отдать официальному propagator выбранного SDK, а fixture оставить как контракт интеграции и регрессионный тест.

Диагностика по симптому

СимптомВероятная границаПроверкаДействие
В gateway и service разные trace-idProxy удалил заголовок, либо выбран 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 при необходимости

Пошаговый runbook

  1. Зафиксируйте границы. Выберите один запрос и выпишите client, gateway, service и downstream. Для каждого перехода укажите ожидаемый режим: forward, participate или restart. Результат — короткая схема, с которой можно сравнить логи.
  2. Снимите вход и выход. В тестовой среде запишите наличие и значение traceparent до proxy и на входе сервиса. Не передавайте в логах пользовательские данные и не делайте trace-id секретом.
  3. Проверьте транспорт. Убедитесь, что маршрут proxy разрешает заголовок, не переписывает его неожиданно и одинаково работает для реального протокола. Если заголовок исчез до сервиса, backend-библиотека пока не подозревается.
  4. Проверьте извлечение. Прогоните валидный fixture, uppercase в значении, нулевые id, неверную длину, ff и выбранную политику для более высокой версии. Для каждого случая зафиксируйте expected decision.
  5. Проверьте span. На валидном входе trace-id должен продолжиться, а текущая операция получить новый локальный span-id. На невалидном входе должен появиться новый локальный trace или controlled fallback SDK, а не исключение из HTTP handler.
  6. Сверьте наблюдаемость. Сведите логи к одному структурному ключу, сравните trace-id и parent-id, а отсутствие записи проверьте с учётом sampling и задержки exporter. Одного совпадения trace-id недостаточно для вывода о latency.
  7. Прогоните маршрут. Выполните end-to-end запрос через реальный proxy и повторите его после изменения конфигурации. Сохраните raw headers, решение извлечения и ссылки на spans; только после этого меняйте rollout или лимиты.

Ограничения и критерий готовности

Этот подход проверяет 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 пропал» остаётся гипотезой.

Проверяемые источники

"}