Files

2 lines
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"index":7,"slug":"editorial-2027-10-field-long-form-interview","title":"Trace Context в HTTP: как сохранить запрос на границе proxy","excerpt":"Если traceparent исчезает или не проходит проверку, gateway и сервис перестают видеть один запрос. Разбираем контракт proxy, безопасный fallback и проверку через реальный HTTP-маршрут.","contentHtml":"<p>Симптом появляется во время сбоя: gateway сообщает timeout, backend пишет 500, а записи нельзя связать одним запросом. Инженер видит несколько похожих событий и не понимает, относятся ли они к одной операции. Цена ошибки — лишний поиск по логам, неверная гипотеза о медленном сервисе и более долгий разбор пользовательской проблемы.</p><p>Чаще ломается не сама трассировка, а граница между компонентами. Proxy не пропускает заголовок, middleware начинает новый trace вместо продолжения, либо сервис принимает строку с неверным форматом. Главный вопрос статьи — как доказать, что один HTTP-запрос сохранил контекст на этой границе, не превратив trace-id в право доступа или бизнес-идентификатор.</p><h2>Короткий ответ: задайте контракт на границе</h2><p>W3C Trace Context задаёт переносимые HTTP-заголовки <code>traceparent</code> и <code>tracestate</code>. Первый несёт общий trace-id, идентификатор родительской операции и flags; второй хранит необязательное состояние конкретных систем трассировки. Контекст связывает участки обработки, но не гарантирует наличие span, корректность логирования или доставку telemetry.</p><p>На границе нужно заранее выбрать один из трёх режимов. Обычный proxy пересылает валидный контекст. Инструментированный proxy участвует в трассе, создаёт свой span и меняет parent-id для следующего участка. Защитный gateway может начать новую трассу на доверенной границе. Последний вариант разрывает внешнюю корреляцию по решению безопасности, поэтому его нельзя выдавать за «потерю заголовка».</p><h2>Выбор режима: что именно делает proxy</h2><table><thead><tr><th>Режим</th><th>Что уходит дальше</th><th>Цена</th><th>Когда применять</th></tr></thead><tbody><tr><td>Forward</td><td>Валидные <code>traceparent</code> и <code>tracestate</code>; proxy не создаёт span</td><td>Дешевле, но задержка внутри proxy не видна отдельным участком</td><td>Тонкий reverse proxy без собственной инструментализации</td></tr><tr><td>Participate</td><td>Тот же trace-id, новый parent-id и span proxy</td><td>Нужны SDK, sampling и единые правила имён</td><td>Когда задержка маршрутизации входит в SLO сервиса</td></tr><tr><td>Restart</td><td>Новый локальный trace; внешний контекст не становится родителем</td><td>Корреляция через границу теряется</td><td>Явная trust boundary или защита от злоупотребления входным контекстом</td></tr></tbody></table><p>Для обычной внутренней HTTP-границы начинайте с <em>forward</em> или <em>participate</em>. Владелец proxy должен записать режим в конфигурации и интеграционном тесте. Иначе команда будет спорить по логам, где разные trace-id могут быть как дефектом, так и намеренным restart.</p><h2>Механизм: формат, извлечение и новый span</h2><p>В формате версии <code>00</code> значение <code>traceparent</code> содержит четыре поля: <code>version-trace-id-parent-id-trace-flags</code>. Это две hex-цифры версии, 32 lowercase hex-цифры trace-id, 16 lowercase hex-цифр parent-id и две lowercase hex-цифры flags. Trace-id и parent-id не могут состоять из одних нулей. Имя HTTP-заголовка регистронезависимо, но отправлять его следует в lowercase; uppercase внутри значений — уже другая проверка.</p><p>Версия <code>00</code> — формат, который проверяет пример ниже. Спецификация описывает правила для будущих версий: pass-through-компонент не должен без причины разбирать неизвестное расширение, а инструмент, который участвует в трассе, обязан иметь политику обработки более высокой версии. Поэтому «не поддерживаем version» — это проектное решение парсера, а не универсальное требование W3C.</p><p>Извлечение выполняет propagator, а не случайный middleware с <code>split('-')</code>. Невалидный carrier не должен приводить к исключению и не должен записываться в контекст как новый родитель. Если валидного входящего контекста нет, SDK создаёт локальный trace по своей политике. Для валидного удалённого контекста текущий сервис создаёт свой span и передаёт downstream новый parent-id, сохраняя trace-id.</p><p>Proxy проверяется отдельно от библиотеки. Сначала выясните, пропускает ли его allow-list заголовок, затем проверьте лимит размера и поведение при нескольких значениях. Нормализация регистра имени — нормальна для HTTP, а удаление поля, непреднамеренный restart или разные правила для HTTP/1.1 и HTTP/2 — уже часть вашего runtime-контракта. Локальный unit test парсера этого не доказывает.</p><p>Trace-id не объясняет задержку сам по себе. Для поиска места ожидания нужны границы span, временные отметки и статус ошибки. OpenTelemetry отдельно определяет HTTP span и его атрибуты; если компоненты пишут только общий идентификатор, связь событий появится, но длительность отдельных участков останется неизвестной.</p><figure><img src=\"/assets/editorial/2027/long-form-interview-2027-editorial-handoff-loop.svg\" alt=\"Клиент передаёт traceparent через gateway, gateway выбирает режим передачи, а сервис проверяет формат перед созданием локального span\"/><figcaption>Сначала проверьте режим proxy, затем формат и создание локального span. Заголовок связывает события, но не является авторизацией.</figcaption></figure><h2>Самодостаточный пример проверки version 00</h2><p>Этот фрагмент можно сохранить как отдельный файл и запустить в Node.js без пакетов. Он проверяет только синтаксический контракт версии <code>00</code>: не создаёт span, не обращается к collector и не утверждает, что заголовок дошёл через proxy. Причина отказа короткая, поэтому её можно считать в метрике без сохранения полного внешнего значения.</p><pre><code>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</code></pre><p>Uppercase trace-id здесь отклоняется, хотя имя заголовка <code>TraceParent</code> само по себе допустимо. Это controlled fallback, а не HTTP 500: сервис не использует повреждённую строку как родительский контекст. В production коде такую проверку лучше отдать официальному propagator выбранного SDK, а fixture оставить как контракт интеграции и регрессионный тест.</p><h2>Диагностика по симптому</h2><table><thead><tr><th>Симптом</th><th>Вероятная граница</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>В gateway и service разные trace-id</td><td>Proxy удалил заголовок, либо выбран restart</td><td>Сравнить raw-заголовок до и после proxy и прочитать режим</td><td>Исправить allow-list или документировать trust boundary</td></tr><tr><td>Контекст есть только локально</td><td>Runtime применяет другой маршрут, лимит или фильтр</td><td>Повторить запрос через тот же gateway с теми же правилами</td><td>Добавить integration test gateway + service</td></tr><tr><td>Валидный на вид заголовок отклонён</td><td>Uppercase в значении, длина, нулевой id или version</td><td>Прогнать fixture на каждое поле и записать reason</td><td>Отбросить вход без 500; не логировать полное значение</td></tr><tr><td>По trace-id не находится событие</td><td>Другой ключ, sampling или задержка доставки</td><td>Проверить структурное поле, span и exporter</td><td>Развести correlation gap и отсутствие telemetry</td></tr><tr><td>После HTTP-запроса теряется связь с очередью</td><td>HTTP carrier не перенесён в message carrier</td><td>Проверить envelope или headers сообщения</td><td>Настроить propagator брокера и отдельный link при необходимости</td></tr></tbody></table><h2>Пошаговый runbook</h2><ol><li><strong>Зафиксируйте границы.</strong> Выберите один запрос и выпишите client, gateway, service и downstream. Для каждого перехода укажите ожидаемый режим: forward, participate или restart. Результат — короткая схема, с которой можно сравнить логи.</li><li><strong>Снимите вход и выход.</strong> В тестовой среде запишите наличие и значение <code>traceparent</code> до proxy и на входе сервиса. Не передавайте в логах пользовательские данные и не делайте trace-id секретом.</li><li><strong>Проверьте транспорт.</strong> Убедитесь, что маршрут proxy разрешает заголовок, не переписывает его неожиданно и одинаково работает для реального протокола. Если заголовок исчез до сервиса, backend-библиотека пока не подозревается.</li><li><strong>Проверьте извлечение.</strong> Прогоните валидный fixture, uppercase в значении, нулевые id, неверную длину, <code>ff</code> и выбранную политику для более высокой версии. Для каждого случая зафиксируйте expected decision.</li><li><strong>Проверьте span.</strong> На валидном входе trace-id должен продолжиться, а текущая операция получить новый локальный span-id. На невалидном входе должен появиться новый локальный trace или controlled fallback SDK, а не исключение из HTTP handler.</li><li><strong>Сверьте наблюдаемость.</strong> Сведите логи к одному структурному ключу, сравните trace-id и parent-id, а отсутствие записи проверьте с учётом sampling и задержки exporter. Одного совпадения trace-id недостаточно для вывода о latency.</li><li><strong>Прогоните маршрут.</strong> Выполните end-to-end запрос через реальный proxy и повторите его после изменения конфигурации. Сохраните raw headers, решение извлечения и ссылки на spans; только после этого меняйте rollout или лимиты.</li></ol><h2>Ограничения и критерий готовности</h2><p>Этот подход проверяет HTTP carrier и передачу контекста между компонентами. Он не проверяет подпись, доверие к отправителю, полноту collector, backend sampling или форматы контекста Kafka и другой очереди. Для асинхронного сообщения нужен отдельный message carrier; иногда правильнее использовать span link, а не притворяться прямым parent-child. Повтор операции и бизнес-связь требуют отдельного безопасного operation-id.</p><p>Готовность доказана, когда интеграционный тест через реальный proxy подтверждает выбранный режим, валидный context получает ожидаемый локальный span, а повреждённый вход приводит к controlled fallback без 500. Логи используют единый trace-id key, span boundaries позволяют искать задержку, а документация явно говорит, где сделан restart. Если эти условия не проверены, проблема «trace-id пропал» остаётся гипотезой.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://www.w3.org/TR/trace-context-2/\" target=\"_blank\" rel=\"noopener\">W3C Trace Context Level 2</a> — формат полей, правила invalid context, forwarding и обработки версий.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/context/api-propagators/\" target=\"_blank\" rel=\"noopener\">OpenTelemetry Propagators API</a> — extract/inject, требования к W3C propagator и поведение при непарсящем carrier.</li><li><a href=\"https://opentelemetry.io/docs/specs/semconv/http/http-spans/\" target=\"_blank\" rel=\"noopener\">OpenTelemetry semantic conventions for HTTP spans</a> — границы HTTP client/server spans и атрибуты для анализа задержки.</li></ul>"}