Проблема видна во время сбоя: 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 для авторизации или помещать в него пользовательские данные.
| Участок | Проверка | Ошибка | Действие |
|---|---|---|---|
| Клиент → gateway | заголовок добавлен один раз | новый trace на каждый redirect | сохранять контекст по правилам клиента |
| Gateway → service | proxy пропускает traceparent | поле удалено | allow-list заголовка и integration test |
| Middleware | формат и ненулевые id | uppercase/короткое поле | отбросить и создать локальный trace |
| Logger | trace-id попал в структурное поле | текстовый grep не находит | единый JSON key и sampling policy |
| Сервис → очередь | HTTP-контекст не теряется при смене транспорта | новое дерево без link | использовать механизм контекста брокера |
Proxy — часть контракта наблюдаемости
Заголовок, дошедший до приложения локально, ничего не доказывает о внешнем маршруте. Реальный proxy может нормализовать имя, удалить неизвестное поле, ограничить размер или создать свой контекст. Поэтому тестировать нужно пару gateway + service с теми же правилами allow-list, а не только чистую функцию парсинга.
Проверка также должна учитывать границы доверия. Внешний клиент может прислать любой trace-id. Это полезный идентификатор для диагностики, но не секрет и не доказательство личности. Логи должны экранировать значение, а sampling не должен удалять единственный event с ошибкой. Наблюдаемость помогает найти проблему, но не должна становиться каналом для доступа.
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Порядок поиска потерянного контекста
- Выберите один запрос и выпишите его границы: клиент, gateway, service и downstream. Для каждой границы нужен отдельный trace-id в логах.
- Проверьте raw-заголовок до proxy и после proxy. Если значение исчезает, не начинайте с backend-библиотеки.
- Добавьте parser fixture на нулевые id, uppercase, неверную длину и запрещённую версию. Ошибка должна быть безопасной и короткой.
- Убедитесь, что middleware продолжает валидный trace и создаёт новый локальный parent-id, а не переписывает весь trace.
- Приведите логи к одному структурному ключу и добавьте ошибку парсинга как счётчик без полного заголовка.
- Прогоните интеграционный тест через реальный 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 пропустит или сервис корректно обработает пользовательский заголовок.