Зачем логи, если есть трейсы

Логичный вопрос после трёх уроков про трейсинг: разве трейсы не покрывают всё? Нет — трейсы и логи решают разные задачи, и в проде нужны оба.

ТрейсЛог
Отвечает на вопрос«Что произошло в этом запросе?»«Что происходит во всех запросах?»
ЕдиницаДерево вызовов одного запросаПлоский поток событий
СилаПричинно-следственная структураПоиск, фильтрация, агрегация по объёму
ХранениеЧасто семплируется (дорого)Дёшево, долго, целиком
Пример«Где сломался запрос X»«Сколько раз за сутки был timeout»

Трейс — это «микроскоп» на один запрос. Лог — «спутниковый снимок» всего потока: по логам строят алерты («ошибок > 1% за 5 минут»), считают частоты, ищут паттерны сбоев. Идеально они работают вместе — и связываются через общий trace_id (об этом ниже).

ℹ️ Три столпа observability

В SRE observability стоит на трёх сигналах: traces (структура запроса — прошлые уроки), metrics (агрегированные числа — следующий урок про дашборды) и logs (дискретные события — этот урок). Ни один не заменяет другие. Логи — самый гибкий и дешёвый сигнал, но только если они структурированы.

Почему structured, а не строки

Классический лог — это строка: "User 42 got error after 3 retries". Человеку читаемо, машине — беда. Чтобы найти «все ошибки пользователя 42», приходится парсить текст регэкспами, а любой рефактор сообщения ломает парсинг. Structured logging переворачивает подход: лог — это не строка, а объект с полями (обычно JSON).

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
❌ Плоская строка "User 42 error after 3 retries in search tool" ✗ поиск — регэкспами по тексту ✗ рефактор сообщения ломает парсинг ✗ не агрегировать, не фильтровать ✓ Structured (JSON) { "event": "tool_error", "user_id": 42, "retries": 3, "tool": "search", "level": "error", "trace_id": "abc123" } ✓ фильтр: user_id=42 AND level=error ✓ агрегация, алерты, связь с трейсом JSON-логи → машина читает поля напрямую → мощный пайплайн: поиск/фильтр агрегация алерты дашборды Grafana Loki · ELK/OpenSearch · Datadog · CloudWatch — все любят 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
Вызов LLMmodel, input_tokens, output_tokens, cost, duration_ms
Вызов инструментаtool, args_summary, success, duration_ms
Retry / fallbackattempt, 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-рендер, привязываешь контекст запроса, пишешь события с полями.

Structured logging агента (structlog)
python
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-процессор), а не «почистим потом». «Потом» не наступает, а данные уже в трёх системах.

Типичные ошибки

❌ Ошибка 1: print вместо логгера

print() — это неструктурированная строка без уровня, времени и полей, которую нельзя ни отфильтровать, ни отключить. Используй настоящий логгер с JSON-выводом с самого начала.

❌ Ошибка 2: логи без trace_id

Без корреляционного идентификатора лог и трейс живут в разных мирах: нашёл ошибку в логах — не можешь открыть её трейс. Привязывай trace_id/request_id ко всем записям запроса.

❌ Ошибка 3: всё на одном уровне

Если каждое событие — INFO (или всё — ERROR), уровни бесполезны: в проде не отключить шум и не выцепить настоящие ошибки. Раскладывай события по DEBUG/INFO/WARNING/ERROR осмысленно.

❌ Ошибка 4: логировать всё «на всякий случай»

Полные промпты и ответы на INFO раздувают объём (деньги за хранение), создают шум и рискуют PII. Логируй осознанно: метаданные и решения, а не весь контент.

Шпаргалка

Логирование агентов — всё в одном месте
text
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 · всё на одном уровне · всё «на всякий»

Практическое задание

Задание: структурируй логи агента

  1. Возьми агента из модулей 03–05 и замени все print на structured-логгер (structlog или стандартный logging с JSON). Убедись, что вывод — валидный JSON построчно.
  2. Разложи события по уровням: что у тебя INFO, что WARNING (retry/fallback), что ERROR? Приведи по примеру каждого.
  3. Привяжи trace_id и user_id через bind() и проверь, что они попали во все записи запроса без ручного проброса.
  4. Залогируй «решение агента»: какой инструмент выбран на какой итерации. Как этот лог поможет в дебаге странного поведения?
  5. Найди в своих логах, где ты рискуешь записать PII/секрет/полный промпт. Перепиши на метаданные (tokens/hash/длина) или добавь маскирование.
  6. Сформулируй один алерт на основе логов (например, «fallback_used > 5% за 10 мин»). Какие поля нужны, чтобы его посчитать?
  7. Со звёздочкой: добавь redaction-процессор, который автоматически маскирует значения полей вроде email, token, password перед выводом.

Что дальше

Структурированные логи и трейсы дают сырые сигналы. Чтобы держать прод под контролем, из них строят дашборды: агрегированные метрики latency, cost, error rate и quality во времени — с алертами. Это финальный урок раздела Observability.