feat(metrics): add named felis_* Prometheus collectors

Introduce internal/metrics exposing the four metric families spec §23
mandates at minimum: felis_servers_total (gauge by desired state),
felis_start_duration_seconds (histogram with Minecraft cold-start
buckets), felis_image_build_failures_total and
felis_reaper_worlds_deleted_total (counters). Collectors are
package-level vars so any subsystem records without an import cycle;
Register wires them into a prometheus.Registerer and is idempotent.

Wire registration into the operator against controller-runtime's global
Registry, so /metrics on the manager's existing metrics endpoint carries
the felis_* families. Instrument the reaper to increment
felis_reaper_worlds_deleted_total in lockstep with Summary.WorldsReaped,
at the one point a world's PVC has actually been deleted.
This commit is contained in:
flyemoji committed 2026-06-30 12:38:55 +09:00
1 parent 50b8487ff5
commit 75642d90cf
5 files changed
+231

No files matched your search

+31
View File
@@ -12,6 +12,9 @@ import (
"time"
"felis.lolicon.best/internal/backup"
"felis.lolicon.best/internal/metrics"
"github.com/prometheus/client_golang/prometheus/testutil"
)
// testNow is the frozen clock for every hermetic case. Idle is expressed as an
@@ -307,6 +310,34 @@ func TestReapIdleWorldFullSequence(t *testing.T) {
}
}
// §23 instrumentation: felis_reaper_worlds_deleted_total advances by exactly one
// per world whose PVC is actually deleted — in lockstep with Summary.WorldsReaped,
// and never for a skipped/preserved world. Asserted as a delta because the counter
// is a process-global singleton other tests in this package also advance.
func TestReapIncrementsDeletedWorldsMetric(t *testing.T) {
before := testutil.ToFloat64(metrics.ReaperWorldsDeletedTotal)
r, _, cl, _ := newReaper(DefaultConfig(),
Candidate{Name: "metric-a", OwnerID: "user-9", LastActiveAt: idleBy(20 * Day)},
Candidate{Name: "metric-b", OwnerID: "user-9", LastActiveAt: idleBy(20 * Day)},
// Fresh server: under the deadline, must NOT be reaped or counted.
Candidate{Name: "metric-fresh", OwnerID: "user-9", LastActiveAt: idleBy(2 * Day)},
)
sum := mustRun(t, r)
if sum.WorldsReaped != 2 {
t.Fatalf("WorldsReaped = %d, want 2", sum.WorldsReaped)
}
if cl.deletePVCCalls != 2 {
t.Fatalf("deletePVC calls = %d, want 2", cl.deletePVCCalls)
}
delta := testutil.ToFloat64(metrics.ReaperWorldsDeletedTotal) - before
if delta != float64(sum.WorldsReaped) {
t.Fatalf("felis_reaper_worlds_deleted_total advanced by %v, want %d (one per reaped world)", delta, sum.WorldsReaped)
}
}
// CENTERPIECE — red line ④: when the archive fails, the PVC is never deleted,
// ownership is untouched, no backup row is written, and the same server is
// retried (state unchanged) on the next run.