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.
Пример правильного лога:
Любое платёжное событие без intent_id в payload — баг. Это первое, что проверяется на PR-ревью.
3. Метрики, которые стоит снимать¶
Ниже — только то, что реально присутствует в коде/БД. Дополнительный Prometheus-экспортёр пока не реализован (см. §5).
3.1. PostgreSQL — состояние очередей и интентов¶
Эти счётчики снимаются прямым SQL-запросом (cron-задача в Admin Panel или внешний экспортёр).
Outbox queue lag — размер pm.outbox_event по статусам:
Интересны статусы PENDING и FAILED — если первый растёт, значит OutboxWorker отстаёт; второй — что-то не доставляется и идёт в DLQ-логику.
PSP transaction map — размер pm.psp_tx_map по PspState:
Используется как индикатор работы 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 планируется добавить отдельный
/metricsendpoint (формат 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.