Перейти к основному содержимому

0013. Базовые метрики приложения (/metrics) на едином реестре prom-client

Копия ADR из репозитория: backend/docs/adr/0013-app-metrics-prometheus.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-06-23
  • Связанные задачи: OP-46 (F6 · наблюдаемость)

Контекст и проблема​

ТЗ 9.6/9.8 требует экспорт базовых метрик приложения в формате Prometheus для наблюдаемости и будущих алертов (стек мониторинга — в F8/DevOps). OP-46 явно называет инструмент prom-client/@willsoto/nestjs-prometheus и набор метрик: HTTP, БД-пул, очереди, WB-квоты.

Ключевое ограничение из уже сделанного: эндпоинт /metrics уже существует — QueueMetricsController (OP-43) отдаёт метрики очередей BullMQ в собранном вручную exposition-формате (без prom-client). Авторы OP-43 явно оставили шов под OP-46 (комментарий в контроллере: «метрики добавятся… либо контроллер расширится через DI дополнительных сборщиков»). Два контроллера на одном пути /metrics невозможны — нужно решение об интеграции.

Дополнительные опоры/ограничения:

  • Контракт OpenAPI (test/openapi.e2e-spec.ts) перечисляет /metrics среди обязательных путей → эндпоинт должен оставаться Swagger-документированным.
  • e2e мокает DataSource/Redis/очереди → сборщики метрик должны быть defensive.
  • Префикс optima_* — соглашение проекта (OP-43) для keep-правила на scraper.

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

  1. Единый реестр prom-client (выбран). @willsoto/nestjs-prometheus владеет /metrics (через кастомный документированный контроллер), метрики очередей OP-43 переносятся в общий Registry как gauges с collect() на scrape.
    • Плюсы: один источник истины, идиоматичный prom-client (default process-метрики, гистограммы HTTP «из коробки»), соответствует формулировке тикета.
    • Минусы: переписывается отгруженный код OP-43 (контроллер + renderPrometheus); текст queue-метрик теперь рендерит prom-client (имена/значения сохранены).
  2. prom-client рядом, OP-43 как есть. Новые метрики — через prom-client, но контроллер OP-43 склеивает свой текст с register.metrics().
    • Минусы: два механизма метрик сосуществуют; хрупкая склейка двух форматов.
  3. Только hand-rolled (без новой зависимости): расширить существующий сервис коллекторами HTTP/БД/WB вручную.
    • Минусы: ручные гистограммы HTTP и process-метрики — много кода и риск ошибок; расходится с формулировкой тикета.

Решение принято человеком (Pavel, 2026-06-23) — вариант 1.

Решение​

MetricsModule (src/core/metrics/) поверх @willsoto/nestjs-prometheus:

  • Эндпоинт. PrometheusModule.register({ controller: MetricsController }) — кастомный MetricsController extends PrometheusController с Swagger-декораторами. Путь /metrics библиотека проставляет на класс сама (Reflect.defineMetadata('path','/metrics',…)), поэтому метод — @Get() без сегмента. Content-Type text/plain; version=0.0.4 даёт базовый контроллер.
  • HTTP. Гистограмма optima_http_request_duration_seconds (labels method/route/status_code), наполняется глобальным HttpMetricsInterceptor. Таймер закрывается по событию finish ответа (после GlobalExceptionFilter → корректный статус). route = шаблон маршрута (req.route.path) для ограничения кардинальности; вне роутов — unmatched.
  • Очереди. QueueMetricsCollector регистрирует optima_queue_jobs и optima_queue_job_duration_ms (имена сохранены из OP-43) с async-collect(), дёргающим QueueMetricsService.collectSnapshots(). renderPrometheus() удалён.
  • Пул БД. DbPoolMetricsCollector → optima_db_pool_connections{state} из DataSource.driver.master (pg Pool), defensive (нет пула → нет series).
  • default process-метрики prom-client включены в dev/prod, выключены под NODE_ENV=test (их event-loop monitor держит активный handle, а в jest нет --forceExit).

Последствия​

  • /metrics теперь единый: default + HTTP + очереди + пул БД в одном реестре.
  • Добавлены зависимости @willsoto/nestjs-prometheus, prom-client.
  • Контракт OpenAPI сохранён (/metrics остаётся документированным).
  • WB-квоты — отложены (follow-up). Текущий WbQuotaService работает per-account (getQuotaForAccount(accountId)); для глобальной метрики нужен cross-account scan ключей wb:rl:* или агрегатор по кабинетам — заводится отдельным тикетом, чтобы не раздувать scope OP-46 (AC покрывают HTTP/очереди).
  • Префикс default-метрик. process_*/nodejs_* оставлены без префикса optima_ — это конвенциональные имена node-exporter дашбордов; keep-правило на scraper при необходимости расширяется до optima_*|process_*|nodejs_*.