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_events7 дней,llm_call_stats_minute90 дней. - 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)