У трассировки два способа не сработать. Первый: открыть трейс во время инцидента и увидеть аккуратную лесенку шагов — вот вход, вот ошибка, вот ответ, но почему шаг решил упасть, не сказано. Второй: открыть трейс и утонуть — шагов больше, чем нужно, в каждом полное тело запроса, и ответ на вопрос «где сломалось» приходится выкапывать из-под мусора.
В обоих случаях сломано одно и то же: схему проектировали не для того, кто её читает. А читает её дежурный — в худший момент и без контекста. Всё, что дальше, выводится из этого рассогласования: спокойный автор схемы против ночного читателя.
Правило одного вопроса
Прежде чем перечислять поля, зафиксируем критерий. Списки ниже из него выводятся, а не придумываются.
Вопрос к каждому полю один: меняет ли его значение следующее действие дежурного? Если да — поле принадлежит трейсу. Если нет — это материал архива, и архивом должна быть другая система хранения: дешёвая и с другим поиском.
Отсюда сразу следует, чем трассировка не является: протоколом всего. Она отвечает на вопрос «что случилось с этим запросом и что делать дальше», и всё, что этому ответу не служит, — издержка. Про разницу между логами и трассировкой шагов у нас есть отдельный материал; здесь важна только её функция при разборе инцидента.
Что писать
Границы шагов. Спан — шаг, у которого есть собственная причина упасть. Проверка: если этот шаг вернёт ошибку, изменится ли ответ запроса или следующее действие дежурного? Не изменится — это не спан, а строка лога. Типичный лишний спан — разбор входных данных, который не падает отдельно от родительского шага.
Вход и выход на границе. Ровно столько, чтобы ответить «что пришло и что ушло», без полного тела. Форма зависит от того, что это за данные:
- объект рендерится из шаблона — промпт, запрос по образцу — пишите идентификатор шаблона и значения переменных: результат рендера восстанавливается, хранить его незачем;
- данные — размер, хэш и укорочанное начало той длины, которую вы себе разрешите; полное тело, если понадобится, лежит по ссылке в другом хранилище;
- идентификаторы — целиком: они маленькие и несущие.
Развилки. Везде, где код выбирает путь, пишите значение предиката и имя выбранной ветки. Не контекст целиком, а исход решения. Без этого трейс показывает, что шаг случился, но не почему пошли именно туда.
Внешние вызовы. Имя сервиса, операция, статус, число повторов. Повтор — событие само по себе: он меняет действие (поднять таймаут? сменить инстанс?).
Ошибки. Текст ошибки дословно, в том спане, где она случилась. Не класс, а текст: «отказ соединения» и «таймаут соединения» ведут к разным следующим шагам.
Длительность. Храните как долю от длительности всего запроса, а не как абсолют. Бюджеты у всех свои, а доля сразу показывает, какой шаг съел запрос.
Чего в трейсе быть не должно
Полное тело запроса, повторённое в каждом спане, — объём, который растёт с трафиком при неизменной информативности. Информация живёт на границах шагов, а не внутри.
Итерации цикла как отдельные спаны. Агентский цикл — самый наглядный случай: каждая итерация порождает дочерний спан, и структура трейса тонет в однотипных повторах. Вместо этого один спан на цикл: сколько итераций, по какой причине вышли, на какой упало.
Производные значения. Писать одновременно размер, хэш, префикс и полное тело — значит не выбрать ничего. Поле, вычислимое из уже записанных, не пишется.
Статичная конфигурация. То, что одинаково у всех запросов сервиса, принадлежит метаданным сервиса, а не каждому трейсу.
Секреты и персональные данные. Что считать чувствительным, решается в момент записи: задним числом из хранилища трейсов это не вычищается — это отдельный проект, а не кнопка. Приём — хэшировать идентификаторы людей и никогда не писать токены.
Поля «на всякий случай». Поле, которое никто не открывает во время инцидента, — не ноль, а минус: оно стоит хранения, замедляет поиск и приучает читателя трейсу не доверять.
Тупик: «запишем всё, отфильтруем потом»
Первая мысль при проектировании схемы обычно такая: записать всё, лишнее отбросить при чтении, а объём победить сэмплированием. Мысль привлекательная — она не требует решений сейчас и звучит как осмотрительность.
Не работает она вот почему.
Сэмплирование не спасает. Чтобы выборка сохранила именно упавший запрос, решение надо принимать, когда исход уже известен. Выборка на входе решает до развязки и выкидывает редкие падения вместе со скучным большинством. Выборка на выходе сначала собирает и записывает трейс целиком — полная цена на пути записи уже заплачена, экономия остаётся только в хранении.
«Потом» не наступает само. У фильтрации нет дедлайна, а у шумных полей появляются потребители: на них строятся графики, от них зависит чужой код. Удалить поле, на которое кто-то смотрит, — ломающее изменение.
Чувствительное, записанное «временно», остаётся навсегда.
И главное: цена мусора падает не на автора схемы, а на читателя. Инструмент, который должен сокращать поиск, удлиняет прокрутку.
Правильный порядок обратный: сначала схема — по правилу одного вопроса, — потом сэмплирование под то, что осталось. А если трассировку у вас сейчас собирают из логов, склеивая их по идентификатору запроса, — это отдельная большая тема, мы разбирали её в материале «Трассировка шагов вместо логов».
Практика: пересобрать схему по шагам
-
Инвентаризация пути. Выпишите стадии запроса от входа до ответа и к каждой примените проверку «своя причина упасть». Прошедшие становятся спанами, непрошедшие вливаются в родителя. Если стадии не вспоминаются по памяти — идите по коду от точки входа; заодно станет ясно, почему трейс был пустым.
-
Представления на границах. Для каждого спана выберите форму входа и выхода: шаблон плюс переменные, размер-хэш-префикс, идентификатор целиком. Условие применимости: если тела у вас маленькие, нечувствительные и хранение дешёвое, полные тела на границах допустимы — вся политика выше про дорогой случай.
-
Развилки. Списком выпишите предикаты, которые выбирают путь, и добавьте к каждому значение и имя ветки. Это самый быстрый способ закрыть дыру из раздела про развилки.
-
Циклы. Один спан на цикл: счётчик итераций, причина выхода, номер падающей итерации. Расширять до поитерационных спанов стоит, только если деталь конкретной итерации меняет действие дежурного.
-
Чувствительное. Белый список полей, которые можно писать; всё новое — через явное добавление. Условие применимости: если схема меняется с каждым релизом, белый список гниёт и его начинают обходить — тогда чёрный список плюс проверка на ревью.
-
Ночной тест. Откройте один здоровый трейс и один упавший. На упавшем ответ «что сломалось и что делать дальше» должен находиться без открытия исходников; если он тонет в прокрутке — в схеме лишнее. Тест бесплатный, гонять его стоит после каждого изменения схемы.
И последнее: схема трассировки — часть регламента дежурства. Разбор инцидента начинается с чтения трейса, и трейс без ответа останавливает регламент на первом шаге. Про сам порядок действий у нас есть материал «Дежурство по агенту».
Позиция редакции
Мы считаем, что трассировка — инструмент принятия решений, а не архив. Писать нужно то, что меняет следующее действие читателя; остальное либо не пишется, либо уходит в другое хранилище по ссылке. Все списки выше — следствия этого тезиса, а не самостоятельные правила.
Мы готовы назвать условие, при котором эта позиция неверна. Если главный потребитель трейса — не человек, а автоматический анализ, критерий «меняет действие дежурного» не применим. Скажем, вы исследуете сами ответы модели как продукт: тогда тело ответа и есть предмет изучения, полнота важнее читаемости, и это уже не трассировка, а датасет — с требованиями датасета.
Близкий случай — молодая система, где ещё неизвестно, как она отказывает. Пока непонятно, какие поля меняют действия, щедрая трассировка — честная разведка, а не неряшливость. Но у разведки должен быть срок: без него она и есть тупик из раздела выше.