DarkRiDDeR15 мин

Trace Context в HTTP: как не потерять запрос на границе proxy

ObservabilityHTTP

Проблема видна во время сбоя: gateway сообщает timeout, backend пишет 500, а связать записи по одному запросу невозможно. Цена потери контекста — инженер читает десятки похожих логов, ошибочно обвиняет медленный сервис и дольше держит пользователя в неопределённости.

Причина часто не в отсутствии системы трассировки, а в границе передачи. Proxy удаляет неизвестный заголовок, middleware создаёт новый trace вместо продолжения или сервис принимает строку с неверным форматом. Исправление требует разделить идентификатор трассы, идентификатор родительского span, правила доверия и способ записи событий.

Что именно передаёт traceparent

Заголовок traceparent содержит четыре поля: версию, trace-id, parent-id и flags. Trace-id связывает дерево обработки одного запроса, parent-id показывает непосредственный предыдущий участок, а flags несут небольшие свойства контекста. Формат строгий: lowercase hexadecimal, правильная длина и ненулевые идентификаторы. Если строка не проходит проверку, её нельзя молча преобразовывать.

Сервис на входе должен решить, продолжать ли внешний trace. Валидный контекст может быть принят как родительский, а новый span создаётся уже локальной библиотекой. Невалидный заголовок лучше отбросить и начать новый локальный trace, одновременно записав безопасную причину. Нельзя доверять trace-id для авторизации или помещать в него пользовательские данные.

Граница передачи HTTP-контекста
УчастокПроверкаОшибкаДействие
Клиент → gatewayзаголовок добавлен один разновый trace на каждый redirectсохранять контекст по правилам клиента
Gateway → serviceproxy пропускает traceparentполе удаленоallow-list заголовка и integration test
Middlewareформат и ненулевые iduppercase/короткое полеотбросить и создать локальный trace
Loggertrace-id попал в структурное полетекстовый grep не находитединый JSON key и sampling policy
Сервис → очередьHTTP-контекст не теряется при смене транспортановое дерево без linkиспользовать механизм контекста брокера

Proxy — часть контракта наблюдаемости

Заголовок, дошедший до приложения локально, ничего не доказывает о внешнем маршруте. Реальный proxy может нормализовать имя, удалить неизвестное поле, ограничить размер или создать свой контекст. Поэтому тестировать нужно пару gateway + service с теми же правилами allow-list, а не только чистую функцию парсинга.

Проверка также должна учитывать границы доверия. Внешний клиент может прислать любой trace-id. Это полезный идентификатор для диагностики, но не секрет и не доказательство личности. Логи должны экранировать значение, а sampling не должен удалять единственный event с ошибкой. Наблюдаемость помогает найти проблему, но не должна становиться каналом для доступа.

Цепочка HTTP-контекста: клиент передаёт traceparent gateway, gateway сохраняет его при пересылке, а сервис проверяет формат и создаёт локальный span.
Схема показывает место проверки на границе сервиса. Заголовок связывает события, но не является авторизацией и не заменяет логику хранения.

Runnable-пример: отклоняем повреждённый traceparent

Парсер получает ровно одну строку и возвращает разобранные поля только после проверки длины, lowercase hexadecimal и ненулевых идентификаторов. Для плохого входа он возвращает причину, которую можно посчитать в метрике без записи полного пользовательского заголовка. Пример не создаёт span и не отправляет telemetry; он демонстрирует проверяемую границу формата.

import { parseTraceparent } from './upgrade-2027-10.mjs';

const valid = parseTraceparent(
  '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
);
const invalid = parseTraceparent(
  '00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01',
);

console.log(valid.ok, valid.traceId.slice(0, 8));
console.log(invalid.ok, invalid.reason);
// true 4bf92f35
// false trace-id-invalid

Порядок поиска потерянного контекста

  1. Выберите один запрос и выпишите его границы: клиент, gateway, service и downstream. Для каждой границы нужен отдельный trace-id в логах.
  2. Проверьте raw-заголовок до proxy и после proxy. Если значение исчезает, не начинайте с backend-библиотеки.
  3. Добавьте parser fixture на нулевые id, uppercase, неверную длину и запрещённую версию. Ошибка должна быть безопасной и короткой.
  4. Убедитесь, что middleware продолжает валидный trace и создаёт новый локальный parent-id, а не переписывает весь trace.
  5. Приведите логи к одному структурному ключу и добавьте ошибку парсинга как счётчик без полного заголовка.
  6. Прогоните интеграционный тест через реальный proxy, затем отдельно проверьте sampling и задержку доставки telemetry.

Почему один идентификатор не решает диагностику

Trace-id связывает события, но не объясняет, сколько времени занял каждый участок. Для этого нужны span-границы, timestamps с понятными часами и статус ошибки. Если gateway и сервис пишут один trace-id, но не записывают начало и конец span, инженер всё ещё не знает, где возникла задержка.

Не следует использовать trace-id как ключ бизнес-операции без отдельного поля. Пользователь может повторить операцию, а один trace может завершиться до асинхронной обработки. Для записи и очереди добавляйте безопасный operation-id и связывайте их в журнале по правилам приватности. Так диагностика не подменяет доменную идемпотентность.

Ограничения и следующий шаг

Парсер не проверяет подпись, доверие к отправителю, sampling backend и форматы контекста Kafka/очереди. Он работает только с header string. Разные библиотеки могут дополнительно проверять version и flags, поэтому интеграционный тест должен закрепить выбранное поведение.

Следующий шаг — добавить один end-to-end тест через proxy: отправить валидный traceparent, проверить один trace-id во всех сервисных логах и отдельно послать повреждённую строку. Вторая проверка должна показать controlled fallback, а не 500 и не принятие внешнего значения как права доступа.

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

  • W3C Trace Context Level 1 — W3C Recommendation, 23 ноября 2021 года, Trace Context Level 1. Применение: Задаёт формат traceparent, правила создания и передачи идентификаторов между HTTP-компонентами. Граница: Не определяет backend storage, sampling, формат логов и семантику бизнес-операции.
  • RFC 9110 — HTTP Semantics — IETF, июнь 2022 года, RFC 9110, Standards Track. Применение: Описывает HTTP-поля и обмен, в котором передаётся контекст трассировки. Граница: Не гарантирует, что proxy пропустит или сервис корректно обработает пользовательский заголовок.