Наблюдаемость агентов

Как не обрушить наблюдаемость агента высокой кардинальностью метрик

Продвинутый уровень До 8 минут Практическое руководство

Промпт, идентификатор сессии или документа кажется полезной меткой — пока каждый запрос не начинает создавать новый временной ряд. Разберём, какие данные оставить в метриках, какие перенести в трассировки и логи и как проверить, что диагностика не пострадала.

Почему кардинальность растёт взрывным образом

Кардинальность — число уникальных комбинаций значений меток. Система метрик хранит отдельный временной ряд для каждой комбинации имени метрики и её labels.

Предположим, агент публикует счётчик с такими измерениями:

agent_requests_total{
  environment,
  model,
  status,
  session_id,
  document_id
}

Даже при 2 окружениях, 4 моделях, 5 статусах, 100 000 сессиях и 20 документах на сессию теоретическое пространство достигает 80 миллионов комбинаций. На практике заполнится лишь часть, но идентификаторы без ограниченного набора значений будут непрерывно создавать новые ряды. Промпт как label ещё опаснее: небольшое изменение текста означает новое значение.

Разделите сигналы по назначению

Сигнал Для чего Допустимые поля Что исключить
Метрики Алерты, SLO, тренды и агрегаты Окружение, стабильное имя модели, операция, класс ошибки, диапазон результата Промпты, UUID, URL с параметрами, точный текст ошибки
Трассировки Путь одного выполнения и задержки этапов Идентификаторы, размеры, результат, имя инструмента, ссылки между spans Секреты и необработанные персональные данные
Логи События, аудит и подробности отказов Структурированные поля, trace ID, нормализованный код ошибки Полные промпты по умолчанию, токены доступа, содержимое документов

Метрика сообщает, что проблема существует. Трассировка показывает, где она возникла. Лог объясняет детали события. Эти сигналы дополняют друг друга, но не должны дублировать весь контекст.

Шаг 1. Составьте бюджет меток

Для каждой метрики выпишите labels и оцените количество значений за срок хранения. Не ограничивайтесь текущим снимком: особенно важна скорость появления новых значений.

metric: agent_request_duration_seconds
labels:
  environment: 2
  operation: 6
  model_family: 4
  outcome: 5
estimated_series_per_bucket: 2 × 6 × 4 × 5 = 240

Для гистограммы умножьте результат на количество bucket-рядов, а также учтите ряды _sum и _count. Это пример расчёта, а не универсальный лимит: бюджет зависит от вашей системы хранения, срока retention и числа реплик.

Удалите или замените поля с неограниченным множеством значений:

  • session_id, request_id, call_id и document_id переносите в span или структурированный лог;
  • prompt замените на контролируемые prompt_template и prompt_version;
  • точный текст ошибки замените на стабильный error_class;
  • URL нормализуйте до имени маршрута, например POST /agents/{agent_id}/runs;
  • числовые величины измеряйте histogram, а не превращайте в строковые labels.

Шаг 2. Оставьте в метриках только ограниченные измерения

Ниже — пример инструментирования на Python. Названия библиотечных методов приведены как конфигурационный шаблон: адаптируйте импорты и инициализацию к используемой версии SDK.

request_counter.add(
    1,
    {
        "environment": "production",
        "operation": "agent.run",
        "model_family": "reasoning",
        "outcome": "success",
        "prompt_template": "document_summary",
        "prompt_version": "v3"
    }
)

request_duration.record(
    elapsed_seconds,
    {
        "environment": "production",
        "operation": "agent.run",
        "model_family": "reasoning",
        "outcome": "success"
    }
)

prompt_template безопасен для кардинальности только тогда, когда его значения берутся из заранее определённого реестра. Если приложение подставляет туда пользовательское имя или хеш полного промпта, проблема лишь меняет форму.

Не добавляйте label ради возможного будущего запроса. Сначала сформулируйте агрегат, дашборд или алерт, которому это измерение действительно нужно.

Шаг 3. Перенесите контекст в трассировку

Идентификаторы полезны внутри конкретного выполнения. Записывайте их как атрибуты span, не превращая в измерения метрик:

with tracer.start_as_current_span("agent.run") as span:
    span.set_attribute("agent.operation", "agent.run")
    span.set_attribute("agent.session.id", session_id)
    span.set_attribute("agent.document.id", document_id)
    span.set_attribute("agent.prompt.template", "document_summary")
    span.set_attribute("agent.prompt.version", "v3")
    span.set_attribute("gen_ai.input.tokens", input_tokens)

    result = run_agent()

    span.set_attribute("agent.outcome", "success")

Не считайте трассировку автоматически безопасным хранилищем. Атрибуты могут индексироваться, а значит влиять на стоимость. UUID обычно допустим для точечного поиска, но полный промпт требует отдельной политики доступа, редактирования и срока хранения.

Если текст промпта нужен для расследований, предпочтительнее хранить:

  • имя и версию шаблона;
  • размер входа и число токенов;
  • признак применения редактирования;
  • ссылку на защищённое хранилище с коротким retention — только при обоснованной необходимости.

Шаг 4. Свяжите логи с трассировкой

Структурированный лог должен содержать trace_id и span_id. Тогда из агрегированного алерта можно открыть пример трассировки, а оттуда перейти к связанным событиям.

{
  "level": "error",
  "event": "agent_tool_call_failed",
  "trace_id": "контекст_текущей_трассировки",
  "span_id": "контекст_текущего_span",
  "tool": "document_search",
  "error_class": "timeout",
  "retryable": true,
  "duration_ms": 2150
}

Значения идентификаторов в примере намеренно условные. Не копируйте в лог тело документа, API-ключ, заголовок авторизации или полный ввод пользователя.

Шаг 5. Поставьте предохранитель на экспорт

Даже после ревизии разработчик может случайно добавить опасный label. На уровне OpenTelemetry Collector можно удалить атрибуты из метрик до отправки в backend:

processors:
  transform/drop_high_cardinality_metric_attributes:
    metric_statements:
      - context: datapoint
        statements:
          - delete_key(attributes, "session_id")
          - delete_key(attributes, "request_id")
          - delete_key(attributes, "call_id")
          - delete_key(attributes, "document_id")
          - delete_key(attributes, "prompt")
          - delete_key(attributes, "error_message")

service:
  pipelines:
    metrics:
      processors:
        - transform/drop_high_cardinality_metric_attributes
        - batch

Это пример защитной конфигурации. Перед применением проверьте синтаксис для установленной версии Collector и объедините процессоры с существующим pipeline. Удаление label меняет агрегирование: два ранее разных ряда будут объединены экспортёром или backend-системой в зависимости от реализации.

Если метрики экспортируются в Prometheus, второй рубеж можно поставить через relabeling:

metric_relabel_configs:
  - action: labeldrop
    regex: 'session_id|request_id|call_id|document_id|prompt|error_message'

labeldrop безопаснее применять по явному списку. Широкое регулярное выражение может удалить label, необходимый для корректного алерта или разделения окружений.

Проверка результата

1. Найдите метрики с наибольшим числом рядов

Для Prometheus-совместимого backend используйте диагностический запрос:

topk(
  20,
  count by (__name__) ({__name__=~".+"})
)

Запустите его до и после изменения на одинаковом временном диапазоне. Сравнивайте также число активных рядов на стороне хранилища: запрос показывает распределение по именам, но не всю стоимость ingestion и retention.

2. Проверьте уникальные значения подозрительных labels

count(count by (session_id) (agent_requests_total))
count(count by (document_id) (agent_requests_total))
count(count by (prompt) (agent_requests_total))

После удаления соответствующего label запрос может вернуть пустой результат — это ожидаемо. Одновременно убедитесь, что разрешённые разрезы остались доступны:

sum by (environment, operation, model_family, outcome) (
  rate(agent_requests_total[5m])
)

3. Проведите контролируемый прогон

  1. В тестовом окружении выполните несколько запросов с разными session ID и document ID.
  2. Убедитесь, что число рядов метрики не растёт пропорционально числу идентификаторов.
  3. Откройте одну трассировку и найдите в ней нужные идентификаторы.
  4. По trace_id найдите связанный структурированный лог.
  5. Проверьте, что алерт можно расследовать по цепочке «метрика → трассировка → лог».

Типовые ошибки

Хешировать промпт и оставить хеш label

Хеш скрывает содержимое, но не уменьшает кардинальность: уникальный промпт всё равно создаёт уникальное значение.

Перенести все поля в логи

Это устраняет взрыв временных рядов, но может создать дорогой полнотекстовый индекс и риск утечки данных. Разделяйте хранение, индексирование и доступ; не каждое записанное поле должно индексироваться.

Использовать точный текст исключения как error label

Сообщения часто содержат URL, позиции, идентификаторы или пользовательский ввод. Нормализуйте их в небольшой набор: timeout, rate_limited, invalid_input, dependency_error, internal.

Считать model ID всегда ограниченным

Прокси, экспериментальные суффиксы и динамические deployment ID могут создавать новые значения. В метрике используйте контролируемое семейство модели, а точный идентификатор оставляйте в span.

Удалить labels без проверки запросов

Дашборды и алерты могут незаметно объединить разные потоки. До изменения найдите обращения к удаляемым labels в правилах, панелях и recording rules.

Полагаться только на sampling

Сэмплирование трассировок сокращает объём traces, но не исправляет кардинальность метрик. Эти механизмы решают разные задачи.

Ограничения подхода

Низкая кардинальность метрик означает отказ от мгновенной агрегации по конкретной сессии или документу. Для точечного расследования придётся переходить в трассировки или логи.

Трассировки могут быть неполными из-за sampling. Для ошибок и редких аномалий полезен tail-based sampling, но он требует буферизации и дополнительных ресурсов. При жёстких требованиях аудита отдельный журнал событий может быть уместнее телеметрии.

Фиксированного безопасного числа рядов не существует. Предел зависит от backend, частоты scrape, retention, репликации, histogram buckets и договорённостей о стоимости. Поэтому полезны не только разовые лимиты, но и наблюдение за скоростью появления новых рядов.

Практическая политика для команды

Метрики:
  разрешено:
    - environment
    - operation
    - model_family
    - outcome
    - error_class
    - prompt_template
    - prompt_version
  запрещено:
    - prompt
    - session_id
    - request_id
    - call_id
    - document_id
    - user_id
    - error_message

Трассировки:
  идентификаторы разрешены при наличии цели поиска
  содержимое пользователя запрещено по умолчанию
  точные версии модели разрешены

Логи:
  обязательны trace_id, event и error_class
  секреты и авторизационные данные запрещены
  чувствительное содержимое требует отдельной политики

Закрепите этот список проверкой конфигурации или код-ревью и заведите алерт на аномальный рост активных рядов. Так защита становится частью эксплуатации, а не разовой уборкой.

Итог

Надёжная схема проста: ограниченные категории идут в метрики, контекст одного выполнения — в трассировки, подробные события — в структурированные логи. Связующим ключом служит trace ID, а не копирование промптов и UUID во все три сигнала.

Продолжить настройку можно по материалам раздела «Руководства». Определения терминов собраны в глоссарии.