Перейти к содержанию

Monitoring & Observability

Какие сигналы Payment Manager отдаёт наружу, какие можно снять самим, и на что вешать алерты. Документ описывает текущее (Phase 1) состояние — без выдуманных метрик и без Prometheus-экспортёра (см. заготовку в конце).


1. Логирование

Единственный логгер сервиса — pino, инициализированный в src/shared/logger.ts:

export const logger = pino({
  level: config.NODE_ENV === 'test' ? 'silent' : 'info',
  transport: config.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true } }
    : undefined,
  formatters: {
    level: (label) => ({ level: label }),
  },
})

Особенности:

  • NODE_ENV=test — логи полностью молчат (level: 'silent'), чтобы Vitest не шумел.
  • NODE_ENV=development — pretty-print через pino-pretty (читаемо в локальном npm run dev).
  • В проде — структурированный JSON в stdout, дальше его подбирает контейнерный логгер (docker / k8s).
  • Поле level сериализуется как строка (info, error, …), а не как число — удобнее фильтровать в Loki/Elastic.

Все модули PM (роуты, саги, воркеры, ledger) импортируют именно этот logger. Другие транспорты (console.log, winston) использовать запрещено.


2. trace_id = intent_id — обязательный инвариант

Из корневого /CLAUDE.md:

trace_id = intent_id — обязателен в каждом платёжном событии в логах

Это значит:

  • Каждый лог-запись, относящаяся к конкретному платёжному намерению, должна содержать поле intent_id (или trace_id, эквивалент).
  • По intent_id восстанавливается полный путь: POST /intents → саги → outbox → PSP → ledger → webhook.
  • В алертах и дашбордах intent_id — основной ключ для корреляции событий между PM, Auth Center и адаптерами PSP.

Пример правильного лога:

logger.info({ intent_id: intent.id, event: 'intent.created', channel: 'IPPS' }, 'intent accepted')

Любое платёжное событие без intent_id в payload — баг. Это первое, что проверяется на PR-ревью.


3. Метрики, которые стоит снимать

Ниже — только то, что реально присутствует в коде/БД. Дополнительный Prometheus-экспортёр пока не реализован (см. §5).

3.1. PostgreSQL — состояние очередей и интентов

Эти счётчики снимаются прямым SQL-запросом (cron-задача в Admin Panel или внешний экспортёр).

Outbox queue lag — размер pm.outbox_event по статусам:

SELECT status, COUNT(*) AS cnt
FROM pm.outbox_event
GROUP BY status;

Интересны статусы PENDING и FAILED — если первый растёт, значит OutboxWorker отстаёт; второй — что-то не доставляется и идёт в DLQ-логику.

PSP transaction map — размер pm.psp_tx_map по PspState:

SELECT state, COUNT(*) AS cnt
FROM pm.psp_tx_map
GROUP BY state;

Используется как индикатор работы psp-worker (Phase 1). Длинный хвост IN_FLIGHT или PENDING_INQUIRY — сигнал, что воркер не справляется или PSP тормозит.

Intent gauge — распределение pm.intent по status:

SELECT status, COUNT(*) AS cnt
FROM pm.intent
WHERE created_at > now() - interval '1 hour'
GROUP BY status;

Особое внимание — status = 'MANUAL_REVIEW': каждый такой интент требует ручного действия оператора Admin Panel.

3.2. Fastify — давление на event loop

Сервер подключает @fastify/under-pressure. В src/admin/health.ts хэндлер GET /health возвращает 503, если плагин считает, что нагрузка превышает порог:

if (app.isUnderPressure()) {
  return reply.status(503).send({ status: 'ko', message: 'Service under pressure' })
}

Плагин внутри отслеживает eventLoopDelay, RSS и heap — пороги задаются в конфиге Fastify. Сейчас наружу метрики не публикуются: единственный публичный сигнал — это код ответа /health (200 / 503).

3.3. BalanceMonitor — события дрейфа баланса

Воркер в src/workers/balance-monitor.ts циклически сравнивает баланс PSP-партнёра (driver.getPartnerBalance()) c TB-счётом NOSTRO/THB. Раз в BALANCE_TICK_MS пишет одно из событий:

Event Уровень Когда
balance_ok info Дрейф в пределах нормы, баланс выше low-threshold.
balance_drift error |tb_nostro − ipps_balance| > IPPS_DRIFT_THRESHOLD_SATANG
low_partner_balance error ipps_balance < IPPS_LOW_BALANCE_THRESHOLD_SATANG
balance_skipped info У драйвера нет getPartnerBalance (например, QP/Wise) — намеренный no-op.

Оба алертных события (balance_drift и low_partner_balance) могут сработать одновременно — фильтры на дашборде не должны «съедать» один из них.


4. Алерты

Все правила навешиваются на лог-агрегатор (Loki/Elastic) либо на SQL-проверку в Admin Panel. Уровни — рекомендация для on-call ротации.

Условие Действие Источник сигнала
event = 'balance_drift' за последние 5 мин page on-call log: src/workers/balance-monitor.ts
event = 'low_partner_balance' за последние 5 мин page on-call log: src/workers/balance-monitor.ts
pm.intent.status = 'MANUAL_REVIEW', COUNT за 5 мин > 0 notification SQL по pm.intent
pm.outbox_event где status = 'PENDING', COUNT > N (тюнить по нагрузке) notification SQL по pm.outbox_event
GET /health → 503 дольше 1 мин подряд page on-call health-endpoint
pm.outbox_event.status = 'FAILED', COUNT > 0 notification SQL по pm.outbox_event
pm.psp_tx_map.state залип в IN_FLIGHT > 10 мин notification SQL: WHERE state='IN_FLIGHT' AND updated_at < now()-interval '10 min'

Пороги (N для outbox lag, окно «5 мин») подбираются по реальной нагрузке — стартовые значения см. в Admin Panel конфигах.


5. Заготовки на будущее

Заготовка на будущее (Phase 2B): Prometheus exporter.

Сейчас все «метрики» — это либо лог-события, либо SQL-агрегации. В Phase 2B планируется добавить отдельный /metrics endpoint (формат Prometheus exposition) с:

  • счётчиками: pm_intent_total{status}, pm_outbox_event_total{status}, pm_psp_tx_map_total{state};
  • гистограммами latency саг и POST /intents;
  • gauge для eventLoopDelay из @fastify/under-pressure (плагин уже его измеряет внутри — нужно только опубликовать);
  • gauge pm_partner_balance_drift_satang{psp} из BalanceMonitor.

До этого момента дашборды строятся над логами (Loki) и read-only SQL-запросами к pm.*.

Заготовка на будущее (Phase 2B): Redis Streams observability.

Когда PSP-адаптеры переедут на Redis Streams (см. корневой CLAUDE.md, секция «Redis Streams»), появятся новые сигналы: длины стримов stream.ipps.*, stream.qp.*, consumer-group lag. Их также имеет смысл выставить через Prometheus.