diff --git a/internal/server/metrics.go b/internal/server/metrics.go index 5893b8e..0b25506 100644 --- a/internal/server/metrics.go +++ b/internal/server/metrics.go @@ -162,11 +162,7 @@ func (t *telemetry) RecordInventory(inv api.InventoryResponse) { } reasonLabels["reason"] = reason setMetric(t.pvcBackupHealthReason, reasonLabels, 1) - if pvc.Healthy { - setMetric(t.pvcBackupHealth, labels, 1) - } else { - setMetric(t.pvcBackupHealth, labels, 0) - } + setMetric(t.pvcBackupHealth, labels, boolGauge(pvc.Healthy)) if pvc.LastBackupAt == "" { continue } @@ -325,6 +321,15 @@ func incMetric(target map[string]metricSample, labels map[string]string) { target[key] = sample } +// boolGauge converts a boolean state into the Prometheus gauge convention of +// 1 for true and 0 for false. +func boolGauge(value bool) float64 { + if value { + return 1 + } + return 0 +} + func setMetric(target map[string]metricSample, labels map[string]string, value float64) { key := metricKey(labels) target[key] = metricSample{labels: cloneLabels(labels), value: value} diff --git a/internal/server/metrics_test.go b/internal/server/metrics_test.go index ab6f924..8431ce8 100644 --- a/internal/server/metrics_test.go +++ b/internal/server/metrics_test.go @@ -1,6 +1,7 @@ package server import ( + "strings" "testing" "time" @@ -160,6 +161,55 @@ func TestTelemetryRecordInventoryPopulatesAndResetsMetrics(t *testing.T) { } } +func TestBoolGaugeMapsHealthToPrometheusValues(t *testing.T) { + if got := boolGauge(true); got != 1 { + t.Fatalf("expected boolGauge(true) to be 1, got %v", got) + } + if got := boolGauge(false); got != 0 { + t.Fatalf("expected boolGauge(false) to be 0, got %v", got) + } +} + +func TestTelemetryRecordInventoryHealthGaugeIdentity(t *testing.T) { + telemetry := newTelemetry() + + telemetry.RecordInventory(api.InventoryResponse{ + Namespaces: []api.NamespaceInventory{ + { + Name: "apps", + PVCs: []api.PVCInventory{ + { + Namespace: "apps", + PVC: "data", + Volume: "pv-apps-data", + Driver: "restic", + Healthy: true, + }, + { + Namespace: "apps", + PVC: "cache", + Volume: "pv-apps-cache", + Driver: "longhorn", + Healthy: false, + HealthReason: "stale", + }, + }, + }, + }, + }) + + rendered := telemetry.render() + for _, line := range []string{ + "# TYPE pvc_backup_health gauge", + `pvc_backup_health{driver="restic",namespace="apps",pvc="data",volume="pv-apps-data"} 1`, + `pvc_backup_health{driver="longhorn",namespace="apps",pvc="cache",volume="pv-apps-cache"} 0`, + } { + if !strings.Contains(rendered, line+"\n") { + t.Fatalf("expected rendered metrics to contain %q, got:\n%s", line, rendered) + } + } +} + func TestTelemetryRecordB2UsageTracksBucketsAndFallbackTimestamp(t *testing.T) { telemetry := newTelemetry() scannedAt := time.Date(2026, 4, 20, 16, 30, 0, 0, time.UTC)