Симптом знаком после появления структурных логов: для одного запроса уже можно найти несколько строк, но из них не видно, где именно прошли 240 миллисекунд. Цена ошибки — править timeout, повтор или базу по догадке. Такая правка может скрыть задержку на время, но следующий запрос снова оставит только разрозненные события. До действия нужен один связанный сценарий, а не новый dashboard.
В этой статье я не подключаю готовую платформу и не показываю trace из чьей-либо системы. Соберу один учебный запрос в памяти: gateway создаёт span, catalog получает его контекст, затем внутри catalog параллельно живут короткий pricing и длинный inventory. Цель скромная: проверить, что общий trace-id не теряется, parent-id указывает на вызывающий span, а waterfall позволяет назвать следующий участок для проверки.
Историческая граница сентября 2020 года
Сначала важно не смешать два слоя. Trace Context Level 1 уже не был черновиком: W3C опубликовал Recommendation 6 февраля 2020 года. Она описывает переносимый HTTP-представитель контекста traceparent и отдельный tracestate для данных конкретного поставщика. Это даёт общий формат передачи, но не рисует service map и не выбирает за нас storage, sampling, exporter или интерфейс разбора.
OpenTelemetry в этот момент полезно воспринимать как развивающуюся спецификацию и набор ранних реализаций, а не как стабильную кнопку «добавить наблюдаемость». Исторический tag v0.5.0 относится к июню 2020 года, а стабильность Trace API официальная документация позже относит к началу 2021-го. Поэтому ниже нет version-specific конфигурации collector-а, auto-instrumentation или обещания, что один пакет одинаково решит браузер, Node и каждую библиотеку. Есть минимальный контракт, который можно проверить отдельно.
| Объект | Что означает в примере | Что не следует в него класть | Как проверяем |
|---|---|---|---|
traceId | одна логическая история из пяти span | пользователя, email, URL с параметрами | все span имеют одинаковое 32-hex значение |
spanId | одна операция внутри истории | название endpoint или текст ошибки | каждый ID уникален и состоит из 16 hex-символов |
parentSpanId | прямая причина появления child span | ID далёкого предка или произвольный request_id | parent существует и охватывает child по времени |
duration | разность учебных endMs - startMs | обещание latency на другой машине | конец строго больше начала |
traceparent | перенос trace-id и текущего parent span через одну границу | секрет, авторизационный заголовок или полный объект запроса | parser принимает только version 00 и валидные ID |
Один trace, а не набор красивых строк
Trace — это не ещё один вид лога. В учебной модели это дерево операций с общей причиной: пользовательское действие попало в gateway, gateway попросил catalog, а catalog вызвал inventory. Span — одна операция с именем, началом, концом и связью с родителем. Если gateway начал gateway.handle, а catalog создал новый случайный trace-id, дерево уже разорвано. В журнале могут остаться две хорошие строки, но вопрос «какой downstream блокировал исходный запрос?» останется без ответа.
Контекст нужен именно на границе. Внутри одной функции можно передать объект аргументом, но после транспорта соседний процесс не знает текущий span автоматически. В W3C version 00 поле traceparent состоит из четырёх частей: version, trace-id, parent-id и trace-flags. В нашем упражнении gateway сериализует trace-id и свой span-id. Catalog извлекает их, создаёт child span и уже его ID передаст дальше. Это не означает, что header принадлежит бизнес-API: это инфраструктурная граница, которую нужно ограничивать и проверять отдельно.
Минимальный header проверяем как данные
Все ID, имена сервисов, интервалы, header и экспорт ниже синтетические. Они собраны в памяти модуля и не являются trace, URL, HTTP-заголовком, аккаунтом или измерением из системы пользователя.
const rootContext = {
traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
spanId: 'a111111111111111',
traceFlags: '01',
};
// Учебный перенос к следующей границе, не реальный исходящий HTTP-запрос.
const traceparent = createTrainingTraceparent(rootContext);
const received = parseTrainingTraceparent(traceparent);
// Получатель создаёт новый span с parentSpanId = received.parentSpanId.
const catalogSpan = {
spanId: 'b222222222222222',
parentSpanId: received.parentSpanId,
traceId: received.traceId,
};
Вызов createTrainingTraceparent() не открывает сеть. Он возвращает строку, которую сразу разбирает parseTrainingTraceparent(). Этот узкий round-trip ловит две разные ошибки. Первая — формат: случайно передали ID неправильной длины, верхний регистр или нулевое значение. Вторая — смысл: child span получил родителя не из извлечённого контекста. Если присутствует только первая проверка, можно получить валидную строку, которая связывает не те операции.
Я намеренно не добавляю сюда tracestate. W3C оставляет его для данных конкретной tracing-системы; в учебном упражнении нет такого владельца. Запись «на будущее» без того, кто её читает, превращается в неподтверждённый vendor contract. То же относится к baggage, произвольным полям пользователя и полным URL. Контекст должен связывать работу, а не становиться обходным каналом для данных, которые нельзя проверить или безопасно хранить.
Собираем waterfall с одним логическим временем
После того как связь существует, можно смотреть duration. В fixture пять интервалов на одной шкале: gateway.handle длится от 0 до 240 ms; catalog.lookup лежит внутри него от 20 до 220 ms; pricing.read занимает 30–70 ms; inventory.fetch — 30–200 ms; его adapter — 100–170 ms. Значения выбраны для объяснения и не являются latency, которую кто-то измерил.
Здесь легко ошибиться с арифметикой. Duration parent span включает время его child span. Поэтому inventory.fetch = 170 ms и inventory.adapter = 70 ms нельзя сложить и назвать 240 ms: adapter уже находится внутри inventory. Для одного учебного дерева можно отдельно посчитать exclusive время — интервалы parent, не перекрытые дочерними span. Это помогает объяснить путь, но не отменяет необходимости сравнить часы и инструментирование в реальном распределённом запуске.
Фикстура доказывает только свой маленький договор
У revision-модуля есть in-memory fixture. Она создаёт синтетические span, валидирует одного root, проверяет parent/child containment, делает round-trip header-а и выбирает child, который заканчивается последним. Затем fixture складывает exclusive сегменты выбранной цепочки. Для именно этой вложенной модели сумма совпадает с root duration 240 ms. Так тест ловит перестановку parent-id, отрицательный интервал и ошибочное сложение пересекающихся duration до того, как текст станет инструкцией.
# Запускается только проверка учебной модели из этого revision-модуля.
node scripts/upgrade-2020-09.mjs --verify-fixture
# Важные части ожидаемого результата:
traceparentRoundTrip: true
childReceivedCurrentParent: true
criticalPathIsExpected: true
criticalPathExclusiveTimeMatchesRoot: true
Важно назвать и то, чего эта команда не доказывает. Она не проверяет реальный proxy, браузер, clock skew между хостами, collector, sampling или экспорт. Она не убеждается, что какой-либо framework автоматически сохранит контекст через callback и очередь. И она не решает вопрос стоимости хранения trace. Это полезный ограничитель: fixture проверяет форму одной истории, а внедрение начинается с одного настоящего маршрута и отдельного теста на каждой транспортной границе.
| Проверка | Что подтверждает | Чего не подтверждает | Следующее действие |
|---|---|---|---|
traceparentRoundTrip | строка version 00 сохраняет trace-id и текущий span-id | что header дошёл через реальный reverse proxy | добавить изолированный transport test в конкретном сервисе |
childReceivedCurrentParent | catalog в модели дочерний к gateway | что все framework middleware создают правильные span | проверить одну входящую и одну исходящую границу |
criticalPathIsExpected | fixture выбрал поздно завершающуюся цепочку | что это единственный bottleneck в production | проверить инструментирование выбранного участка |
invalidHeaderRejected | нулевой trace-id не проходит parser | политику доверия к внешнему клиенту | описать, где входной контекст принимается, а где создаётся заново |
Маршрут первой проверки
- Выбрать один учебный пользовательский путь и назвать цену задержки без диагноза: например, «результат ждёт один downstream, но логи не показывают порядок».
- Задать один trace-id и пять заранее перечисленных span. Не включать в ID данные пользователя, маршрут с параметрами или текст исключения.
- На первой границе создать root span; перед следующей границей сериализовать только version 00, trace-id, текущий span-id и flags.
- На получателе валидировать format, создать child с извлечённым parent-id и проверить, что trace-id не сменился.
- Запустить
--verify-fixture; если red, исправить контракт или интервалы, а не переставлять таймауты. - Только затем выбрать один реальный транспорт и добавить отдельный test, который проверяет передачу контекста без настоящих пользовательских данных.
Где этот приём останавливается
Этот материал не описывает полноценную distributed tracing platform. У него нет backend-а поиска, service map, collector configuration, retention, sampling policy и автоматической инструментации. В сентябре 2020 года было бы неправдоподобно написать, что эти куски уже одинаково стабильны во всех стеках. Нормальный следующий шаг — не массово оборачивать всё приложение, а выбрать одну синхронную границу, сохранить version/adapter рядом с тестом и сначала увидеть один честный waterfall.
Если задача включает очередь, fan-out или несколько родителей, дерево fixture уже недостаточно. Там появляются links, асинхронное время и отдельное решение о том, считать ли продолжение тем же trace. Если часы разных процессов не согласованы, абсолютное наложение на waterfall тоже требует проверки. В этих случаях причина звучит конкретно: модель слишком мала для новой границы. Действие тоже конкретно: не рисовать уверенный critical path до того, как появится проверяемый transport и время.
Проверяемые источники
- W3C Trace Context Level 1 — Recommendation, 06 February 2020 — историческая Recommendation задаёт поля traceparent версии 00, правила проверки trace-id/parent-id и различает перенос контекста с участием в trace
- OpenTelemetry Specification v0.5.0 — historical changelog — tagged revision от 2 июня 2020 года; версия до 1.0 фиксирует развивающуюся спецификацию, а не готовую кросс-языковую платформу
- OpenTelemetry: Libraries — historical stability note — официальная документация относит стабильность Trace API к началу 2021 года; это редакционная граница, из-за которой статья сентября 2020 не обещает стабильный SDK