Наблюдаемость
api и worker предоставляют служебный (ops) листенер (по умолчанию 0.0.0.0:9090,
настраивается через METRICS_ADDR, пустое значение отключает) со следующими эндпоинтами:
| Эндпоинт | Назначение |
|---|---|
GET /metrics | Эндпоинт сбора метрик Prometheus |
GET /healthz | Liveness — 200, пока процесс запущен, без проверки зависимостей |
GET /readyz | Readiness — пинг Postgres, пинг Redis, миграции актуальны |
Служебный порт намеренно не защищён аутентификацией: метрики раскрывают операционные
детали (имена кластеров, частоту ошибок), поэтому привязывайте его к внутреннему
интерфейсу/сети и снимайте метрики оттуда. Никогда не публикуйте его через обратный
прокси. В dev-стеке compose api пробрасывается на хостовый порт 9090, а worker — на
9091.
Кроме того, api оставляет /healthz и /readyz на своём публичном API-порту (часть
контракта OpenAPI); обе поверхности выполняют одни и те же проверки.
Семантика readiness
/readyz возвращает 503 {"status":"not_ready","dependency":"..."} с именем первой
отказавшей зависимости: postgres, redis или migrations.
Проверка migrations сравнивает версию схемы в goose_db_version с самой новой
миграцией, встроенной в бинарник на момент сборки: база данных, отстающая от
бинарника, не готова (сначала примените миграции — автоматически они никогда не
применяются); база данных, опережающая бинарник, принимается (rolling upgrade).
Ни разу не мигрированная база данных не готова.
Конфигурация scrape для Prometheus
scrape_configs:
- job_name: planeon-api
static_configs:
- targets: ["your-host:9090"]
- job_name: planeon-worker
static_configs:
- targets: ["your-host:9091"]
Справочник метрик
Кардинальность ограничена намеренно: метки — это kind задания, имя cluster,
state сессии, status, outcome и method HTTP — никогда не vmid, id задания или
путь запроса.
| Метрика | Тип | Метки | Значение |
|---|---|---|---|
planeon_jobs_depth | gauge | kind, status, cluster | Задания в очереди/в работе прямо сейчас (экспортируют и api, и worker — агрегируйте экспортеры через max by; cluster пуст для заданий, не привязанных к кластеру) |
planeon_jobs_oldest_queued_age_seconds | gauge | kind, cluster | Возраст самого старого задания в очереди; 0, когда очередь пуста |
planeon_jobs_processed_total | counter | kind, outcome | Исходы выполнения раннера: succeeded, retried, failed (финальный), released (повторная постановка в очередь при graceful shutdown), lease_lost (результат отброшен после reclaim) |
planeon_job_duration_seconds | histogram | kind | Время выполнения обработчика |
planeon_job_lease_reclaims_total | counter | kind | Просроченные lease worker-процессов, возвращённые в оборот (задание поставлено в очередь заново или провалено после падения worker) |
planeon_reconcile_duration_seconds | histogram | — | Один цикл reconcile по всем активным пулам |
planeon_reconcile_cycles_total | counter | outcome | Циклы reconcile: ok / error |
planeon_pve_requests_total | counter | cluster, method, outcome | Вызовы Proxmox API: success / error / timeout |
planeon_pve_request_duration_seconds | histogram | cluster | Время round-trip вызова Proxmox API |
planeon_sessions | gauge | state, cluster | Сессии по состоянию жизненного цикла и кластеру PVE |
planeon_db_pool_* | gauge/counter | — | Пул pgx: соединения (max/total/idle/acquired/constructing), счётчики acquire и суммарное время ожидания |
go_*, process_* | — | — | Стандартные коллекторы рантайма |
Gauge-метрики очереди и сессий намеренно вычисляются из Postgres на момент scrape
обоими приложениями: если worker недоступен, растущая очередь всё равно видна
через api. Настраивайте дашборды на конкретный job или агрегируйте через
max by (...).
Дашборд Grafana
Скачайте planeon-overview.json
(раздаётся этим сайтом документации), импортируйте его в Grafana и выберите
свой источник данных Prometheus. Панели сгруппированы в ряды Availability, Jobs,
Reconcile, PVE, Sessions и DB pool; единицы измерения и пороги согласованы с
рекомендациями по алертингу ниже. Переменные вверху фильтруют все панели:
$job / $instance выбирают цель скрейпа (api или worker — оба экспортируют
gauge-метрики из БД), $cluster фильтрует по имени кластера PVE, а $tenant
— задел на будущее: остаётся в значении «All», пока метрики не получат
лейбл tenant.
Рекомендации по алертингу
Платформа не поставляется со встроенным алертингом — используйте собственный стек. Рекомендуемые правила:
groups:
- name: planeon
rules:
- alert: PlaneonReadyzDown
expr: up == 0
for: 3m
annotations:
summary: "Planeon {{ $labels.job }} scrape target down"
- alert: PlaneonQueueBacklogAge
expr: max(planeon_jobs_oldest_queued_age_seconds) > 300
for: 10m
annotations:
summary: "Jobs waiting more than 5 minutes — worker down or stuck?"
- alert: PlaneonJobFailures
expr: sum(rate(planeon_jobs_processed_total{outcome="failed"}[15m])) > 0
for: 15m
annotations:
summary: "Jobs failing terminally"
- alert: PlaneonLeaseReclaims
expr: sum(increase(planeon_job_lease_reclaims_total[15m])) > 0
annotations:
summary: "A worker crashed mid-job or stalled past its lease — check worker restarts/logs"
- alert: PlaneonPVEErrors
expr: |
sum by (cluster) (rate(planeon_pve_requests_total{outcome!="success"}[10m]))
/ sum by (cluster) (rate(planeon_pve_requests_total[10m])) > 0.2
for: 10m
annotations:
summary: "More than 20% of PVE API calls failing on {{ $labels.cluster }}"
- alert: PlaneonDBPoolSaturated
expr: planeon_db_pool_acquired_conns >= planeon_db_pool_max_conns
for: 5m
annotations:
summary: "pgx pool exhausted on {{ $labels.instance }}"