Зачем логи, если есть трейсы
Логичный вопрос после трёх уроков про трейсинг: разве трейсы не покрывают всё? Нет — трейсы и логи решают разные задачи, и в проде нужны оба.
| Трейс | Лог | |
|---|---|---|
| Отвечает на вопрос | «Что произошло в этом запросе?» | «Что происходит во всех запросах?» |
| Единица | Дерево вызовов одного запроса | Плоский поток событий |
| Сила | Причинно-следственная структура | Поиск, фильтрация, агрегация по объёму |
| Хранение | Часто семплируется (дорого) | Дёшево, долго, целиком |
| Пример | «Где сломался запрос X» | «Сколько раз за сутки был timeout» |
Трейс — это «микроскоп» на один запрос. Лог — «спутниковый снимок» всего потока: по логам строят алерты («ошибок > 1% за 5 минут»), считают частоты, ищут паттерны сбоев. Идеально они работают вместе — и связываются через общий trace_id (об этом ниже).
В SRE observability стоит на трёх сигналах: traces (структура запроса — прошлые уроки), metrics (агрегированные числа — следующий урок про дашборды) и logs (дискретные события — этот урок). Ни один не заменяет другие. Логи — самый гибкий и дешёвый сигнал, но только если они структурированы.
Почему structured, а не строки
Классический лог — это строка: "User 42 got error after 3 retries". Человеку читаемо, машине — беда. Чтобы найти «все ошибки пользователя 42», приходится парсить текст регэкспами, а любой рефактор сообщения ломает парсинг. Structured logging переворачивает подход: лог — это не строка, а объект с полями (обычно JSON).
Как только событие — это поля, всё становится возможным: «покажи все tool_error где tool=search и retries>=3 за последний час» — это запрос, а не археология по тексту. Плюс JSON-логи нативно принимают все системы агрегации (Grafana Loki, ELK/OpenSearch, Datadog, CloudWatch).
Меняй мышление: лог пишется для системы поиска, не для чтения глазами в терминале. Поэтому главное — поля (event, user_id, tool, latency_ms…), а человекочитаемое сообщение вторично. В деве можно рендерить красиво в консоль, в проде — чистый JSON в stdout.
Уровни и что логировать
Не всё подряд и не молча. Два инструмента дозировки: уровни (насколько важно) и осознанный выбор что писать.
Уровни — стандартная лестница важности, чтобы в проде оставить нужное, а шум отключить:
DEBUG— детали для локальной отладки (полные промпты, промежуточные шаги). В проде обычно выключен.INFO— нормальные бизнес-события: запрос принят, инструмент вызван, ответ отдан.WARNING— что-то не так, но система справилась: retry сработал, сработал fallback, близко к лимиту.ERROR— операция не удалась: инструмент упал, провайдер недоступен, запрос провалился.CRITICAL— система в опасности: отказ ключевой зависимости, невозможность обслуживать.
Что логировать у LLM-агента (события, специфичные для агентов):
| Событие | Полезные поля |
|---|---|
| Запрос принят / завершён | request_id, trace_id, user_id, latency_ms, status |
| Вызов LLM | model, input_tokens, output_tokens, cost, duration_ms |
| Вызов инструмента | tool, args_summary, success, duration_ms |
| Retry / fallback | attempt, reason, next_action (level=warning) |
| Ошибка | error_type, message, trace_id (level=error) |
| Решение агента | chosen_tool, step, iteration |
Специфика агентов: полезно логировать не только «что вернулось», но и «почему агент так решил» — какой инструмент выбрал, на какой итерации, сколько токенов потратил. Именно эти события потом объясняют странное поведение в проде, которое по одному финальному ответу не понять. Но помни про баланс: слишком подробный лог на INFO = шум и деньги за хранение.
Корреляция логов и трейсов
Логи и трейсы становятся по-настоящему мощными, когда связаны. Ключ связи — trace_id (из прошлого урока про OpenTelemetry): если каждая лог-запись несёт trace_id текущего запроса, то из алерта по логам ты одним кликом прыгаешь в полный трейс этого запроса, и наоборот.
Алерт: "ошибок tool_error > 1% за 5 мин" (по ЛОГАМ — агрегация)
│
▼ берём trace_id из лог-записи
Трейс запроса abc123 (по ТРЕЙСУ — структура)
│ видим: search вернул пусто → LLM галлюцинировал
▼
Фикс + лог с level=info "fallback_used" (проверяем, что помогло)
Технически это делают через контекстные поля: в начале обработки запроса привязываешь trace_id, request_id, user_id к логгеру — и они автоматически попадают в каждую запись этого запроса, без ручного проброса в каждый вызов.
Практика: structured logging на structlog
В Python стандартный logging умеет JSON, но популярнее structlog — он изначально про поля и контекст. Логика: настраиваешь JSON-рендер, привязываешь контекст запроса, пишешь события с полями.
import structlog
# 1. JSON-вывод в stdout (в проде это забирает агрегатор логов)
structlog.configure(
processors=[
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
log = structlog.get_logger()
def handle_request(user_id: str, question: str, trace_id: str):
# 2. Контекст запроса → попадёт в КАЖДУЮ запись ниже автоматически
request_log = log.bind(trace_id=trace_id, user_id=user_id)
request_log.info("request_received") # INFO: бизнес-событие
try:
answer, usage = run_agent(question)
request_log.info(
"llm_call", # поля, не строка!
model="gpt-4o",
input_tokens=usage.input, output_tokens=usage.output,
cost=usage.cost, duration_ms=usage.ms,
)
return answer
except ToolError as e:
request_log.warning("fallback_used", reason=str(e)) # WARNING: справились
return safe_fallback()
except Exception as e:
request_log.error("request_failed", error_type=type(e).__name__) # ERROR
raise
# Вывод (одна строка = один JSON-объект):
# {"event":"llm_call","level":"info","timestamp":"2026-07-19T...","trace_id":"abc123",
# "user_id":"42","model":"gpt-4o","input_tokens":1120,"output_tokens":340,"cost":0.01}
Обрати внимание: trace_id и user_id задаются один раз через bind() и сами попадают во все последующие записи — вот та самая корреляция и контекст без ручного проброса.
Что НЕЛЬЗЯ логировать
Самая опасная часть. Логи у агентов особенно соблазнительно наполнить полными промптами и ответами — но там почти всегда есть чувствительное. Логи хранятся долго, копируются в агрегаторы, доступны многим — утечка через логи это классический инцидент.
- Секреты — API-ключи, токены, пароли, строки подключения. Никогда.
- PII — имена, email, телефоны, адреса, данные документов. Маскируй или не пиши.
- Полные промпты/ответы на INFO — там и PII, и объём. Для контента — трейсы (с маскированием) или DEBUG в деве, не прод-логи.
- Чувствительные бизнес-данные — платёжки, медзаписи и т.п. по требованиям комплаенса.
Правило-спасатель: пиши про данные, а не сами данные. Не prompt="Мой паспорт 1234...", а prompt_tokens=1120, prompt_hash="a3f...". Не сам ответ, а его длину/статус/оценку. Если контент всё-таки нужен для дебага — прогоняй через маскирование PII на этапе логирования (redaction-процессор), а не «почистим потом». «Потом» не наступает, а данные уже в трёх системах.
Типичные ошибки
print() — это неструктурированная строка без уровня, времени и полей, которую нельзя ни отфильтровать, ни отключить. Используй настоящий логгер с JSON-выводом с самого начала.
Без корреляционного идентификатора лог и трейс живут в разных мирах: нашёл ошибку в логах — не можешь открыть её трейс. Привязывай trace_id/request_id ко всем записям запроса.
Если каждое событие — INFO (или всё — ERROR), уровни бесполезны: в проде не отключить шум и не выцепить настоящие ошибки. Раскладывай события по DEBUG/INFO/WARNING/ERROR осмысленно.
Полные промпты и ответы на INFO раздувают объём (деньги за хранение), создают шум и рискуют PII. Логируй осознанно: метаданные и решения, а не весь контент.
Шпаргалка
3 СТОЛПА OBSERVABILITY: traces (структура) · metrics (числа) · LOGS (события)
трейс = «микроскоп» на 1 запрос · лог = «спутник» на весь поток
логи: дёшево, целиком, для поиска/агрегации/алертов
STRUCTURED > СТРОКИ: лог = JSON-объект с ПОЛЯМИ, не текст
→ фильтр (user_id=42 AND level=error) · агрегация · алерты · связь с трейсом
правило: логи для МАШИН, сообщение вторично, поля главное
принимают: Grafana Loki, ELK/OpenSearch, Datadog, CloudWatch
УРОВНИ: DEBUG (дев) · INFO (бизнес-события) · WARNING (справились: retry/fallback)
· ERROR (операция упала) · CRITICAL (система в опасности)
ЧТО ЛОГИРОВАТЬ (агент): request принят/завершён · llm_call (model/tokens/cost)
· tool_call · retry/fallback · error · РЕШЕНИЯ агента (какой tool, итерация)
логируй ПОЧЕМУ решил, а не только результат
КОРРЕЛЯЦИЯ: trace_id в КАЖДОЙ записи → алерт по логам ↔ полный трейс
bind(trace_id, request_id, user_id) один раз → во все записи запроса
НЕЛЬЗЯ: секреты · PII · полные промпты/ответы на INFO · чувствит. бизнес-данные
ПРАВИЛО: логируй МЕТАДАННЫЕ, не содержимое (tokens/hash/длина, не сам текст)
redaction на этапе логирования, не «почистим потом»
ГРАБЛИ: print вместо логгера · без trace_id · всё на одном уровне · всё «на всякий»
Практическое задание
Задание: структурируй логи агента
- Возьми агента из модулей 03–05 и замени все
printна structured-логгер (structlog или стандартный logging с JSON). Убедись, что вывод — валидный JSON построчно. - Разложи события по уровням: что у тебя INFO, что WARNING (retry/fallback), что ERROR? Приведи по примеру каждого.
- Привяжи
trace_idиuser_idчерезbind()и проверь, что они попали во все записи запроса без ручного проброса. - Залогируй «решение агента»: какой инструмент выбран на какой итерации. Как этот лог поможет в дебаге странного поведения?
- Найди в своих логах, где ты рискуешь записать PII/секрет/полный промпт. Перепиши на метаданные (tokens/hash/длина) или добавь маскирование.
- Сформулируй один алерт на основе логов (например, «fallback_used > 5% за 10 мин»). Какие поля нужны, чтобы его посчитать?
- Со звёздочкой: добавь redaction-процессор, который автоматически маскирует значения полей вроде
email,token,passwordперед выводом.
Что дальше
Структурированные логи и трейсы дают сырые сигналы. Чтобы держать прод под контролем, из них строят дашборды: агрегированные метрики latency, cost, error rate и quality во времени — с алертами. Это финальный урок раздела Observability.