ADR-0062: Телеметрия LLM-вызовов и реальный экран воркеров

Status: accepted Date: 2026-06-02 Deciders: Максим Белканов, платформенная команда

Контекст

Раздел админки «LLM / воркеры» (/queues) работал на demo-данных. Реальной per-call телеметрии в БД не было — только slog. Нужно видеть фактическую нагрузку: токены/час, запросы/час, латентность (p95/p99), долю ошибок, backlog и стоимость по каждому потребителю LLM, чтобы управлять лимитами gateway и понимать, какой воркер их съедает.

Решение

  • Таблица llm_call_events (per-call: ts, component, model, outcome, токены, duration_ms, error_class). Пишется из hot-path (OpenAIClient.logCompletionEvent) через асинхронный fail-soft sink (asyncCallEventSink): bounded-канал + батч-flush + drop-on-full, чтобы не добавлять задержку и не ронять LLM-вызов при сбое записи. Запись — pgx.CopyFrom.
  • Агрегации считаются прямо по сырым событиям за окно (ReadWindowStats, percentile_cont в БД): объём за час мал (прод ~сотни вызовов/час). От отдельного rollup-воркера отказались — это убирает лишний бинарь и deploy/CI-обвязку. Таблица llm_call_stats_minute создана на будущее (не наполняется). Spark за 30 минут — date_trunc('minute')-запрос в хендлере.
  • llm_model_pricing (редактируемая прайс-карта per-1M-токенов) + чистая CostFor. Плитка стоимости показывается только при заданной цене.
  • Backlog по воркерам — best-effort count(*) по таблицам-источникам каждого воркера (тот же предикат, что в batch-select); не-считаемые роли (taxonomy/analog_rules/estimate/embedding) опущены.
  • Ретеншн через unified retention BC (ADR-0058): llm_call_events 7 дней, llm_call_stats_minute 90 дней.
  • Admin API GET /api/v1/admin/llm/workers (модель из ModelResolver (ADR-0061) + window-stats + backlog + cost + spark) и GET/PUT /api/v1/admin/llm/pricing. Экран /queues переведён с demo на реальные данные.

Последствия

Плюсы

  • Реальная видимость нагрузки и стоимости per-component, near-real-time (refetch 5 c).
  • Hot-path не замедляется (неблокирующий sink, fail-soft).
  • Минимум новых процессов — нет отдельного rollup-воркера, проще деплой.

Минусы

  • Перцентили считаются по сырым событиям на каждый запрос экрана (ограничено 7-дневным ретеншном и окном запроса — дёшево на текущем объёме; при кратном росте трафика вернуть minute-rollup).
  • Эмбеддинги не инструментированы (отдельный code path, не logCompletionEvent) — роль embedding показывает модель/backlog, но без call-статистики.
  • recent_1m — прокси активности (вызовов за минуту), а не истинный inflight; в UI подписан честно.

Нейтральные последствия

  • llm_call_stats_minute существует, но пуст до возможного включения rollup.

Рассмотренные альтернативы

Отдельный rollup-воркер (3-tier, как cabinet-stats)

Точнее на больших объёмах, но новый бинарь + deploy/CI-обвязка + курсорная логика. Отклонено для текущего объёма; прямой запрос по сырым событиям достаточен.

Метрики из Prometheus (otelprom)

Требует доступа api-server к Prometheus + PromQL-клиент. Отклонено: backlog всё равно из БД, а per-component токены проще из ledger/событий.

Ссылки

  • Спека: docs/superpowers/specs/2026-06-02-admin-llm-model-config-and-worker-telemetry-design.md (§6)
  • План: docs/superpowers/plans/2026-06-02-llm-worker-telemetry.md
  • Связанный: ADR-0061 (динамический выбор модели), ADR-0058 (unified retention)