Skip to main content

Observability

The api and worker each expose an ops listener (default 0.0.0.0:9090, configured by METRICS_ADDR, set-but-empty disables) serving:

EndpointPurpose
GET /metricsPrometheus scrape endpoint
GET /healthzLiveness — 200 while the process runs, no dependency checks
GET /readyzReadiness — Postgres ping, Redis ping, migrations current

The ops port is not authenticated by design: metrics expose operational detail (cluster names, error rates), so bind it to an internal interface / network and scrape it there. Never publish it through your reverse proxy. In the dev compose stack the api maps to host 9090 and the worker to 9091.

The api additionally keeps /healthz and /readyz on its public API port (part of the OpenAPI contract); both surfaces run the same checks.

Readiness semantics

/readyz returns 503 {"status":"not_ready","dependency":"..."} naming the first failing dependency: postgres, redis, or migrations.

The migrations gate compares the schema version in goose_db_version against the newest migration embedded in the binary at build time: a database behind the binary is not ready (apply migrations first — they are never applied automatically); a database ahead of the binary is accepted (rolling upgrade). A never-migrated database is not ready.

Prometheus scrape configuration

scrape_configs:
- job_name: planeon-api
static_configs:
- targets: ["your-host:9090"]
- job_name: planeon-worker
static_configs:
- targets: ["your-host:9091"]

Metric reference

Cardinality is bounded by design: labels are job kind, cluster name, session state, status, outcome and HTTP method — never vmid, job id or request path.

MetricTypeLabelsMeaning
planeon_jobs_depthgaugekind, status, clusterJobs queued/running right now (exported by api and worker — aggregate exporters with max by; cluster is empty for jobs not tied to one)
planeon_jobs_oldest_queued_age_secondsgaugekind, clusterAge of the oldest queued job; 0 when the queue is empty
planeon_jobs_processed_totalcounterkind, outcomeRunner outcomes: succeeded, retried, failed (terminal), released (graceful-shutdown requeue), lease_lost (result discarded after reclaim)
planeon_job_duration_secondshistogramkindHandler execution time
planeon_job_lease_reclaims_totalcounterkindExpired worker leases reclaimed (job requeued or failed after a worker crash)
planeon_reconcile_duration_secondshistogramOne reconcile cycle across active pools
planeon_reconcile_cycles_totalcounteroutcomeReconcile cycles: ok / error
planeon_pve_requests_totalcountercluster, method, outcomeProxmox API calls: success / error / timeout
planeon_pve_request_duration_secondshistogramclusterProxmox API round-trip latency
planeon_sessionsgaugestate, clusterSessions by lifecycle state and PVE cluster
planeon_db_pool_*gauge/counterpgx pool: conns (max/total/idle/acquired/constructing), acquire counters and cumulative wait
go_*, process_*Standard runtime collectors

Queue and session gauges are computed from Postgres at scrape time by both apps on purpose: if the worker is down, the growing queue is still visible through the api. Point dashboards at one job or aggregate with max by (...).

Grafana dashboard

Download planeon-overview.json (served by this docs site), import it into Grafana, and select your Prometheus datasource. Panels are grouped into Availability, Jobs, Reconcile, PVE, Sessions and DB pool rows, with units and thresholds matching the alerting suggestions below. Template variables at the top filter every panel: $job / $instance select the scrape target (api vs worker — both export the DB-derived gauges), $cluster filters by PVE cluster name, and $tenant is a forward-compatibility placeholder that stays on "All" until metrics carry a tenant label.

Alerting suggestions

No alerting ships with the platform — bring your own stack. Suggested rules:

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 }}"