Wire the provider health monitor into the manager, with gauges

--provider-check-interval (1m; 0 = off, else at least 10s) and
--provider-check-timeout (15s) are validated before the providers config
loads. Checkers are collected from the unwrapped providers in config
order, so checks never count as provider requests. Three scrape-time
gauges per {provider,target} read the monitor's snapshot: up, check
duration and check timestamp. The e2e suite waits for the kubernetes
ProviderHealth to turn Ready and asserts its gauge in the scrape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-11 20:23:55 +00:00
co-authored by Claude Opus 5
parent 88a15ecdb5
commit 9a3ee963a0
6 changed files with 215 additions and 3 deletions
+48
View File
@@ -117,3 +117,51 @@ func (c *constCollector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(c.desc, prometheus.GaugeValue, float64(value), label)
}
}
// ProviderTarget is one provider health-check target as the gauges see it.
type ProviderTarget struct {
Provider, Target string
Up bool
Duration time.Duration
CheckedAt time.Time
}
// RegisterProviderTargets registers the provider-check gauges. Like the
// other scrape-time collectors they are read from the source at every
// scrape, so a target that goes away stops being reported rather than
// leaving a stale series behind.
func RegisterProviderTargets(reg prometheus.Registerer, read func() []ProviderTarget) error {
labels := []string{"provider", "target"}
return reg.Register(&providerTargetCollector{
up: prometheus.NewDesc("proxy_operator_provider_target_up",
"Whether the target passed its latest provider health check (1) or not (0).", labels, nil),
duration: prometheus.NewDesc("proxy_operator_provider_check_duration_seconds",
"Duration of the target's latest provider health check.", labels, nil),
checked: prometheus.NewDesc("proxy_operator_provider_check_timestamp_seconds",
"Unix time of the target's latest provider health check.", labels, nil),
read: read,
})
}
type providerTargetCollector struct {
up, duration, checked *prometheus.Desc
read func() []ProviderTarget
}
func (c *providerTargetCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.up
ch <- c.duration
ch <- c.checked
}
func (c *providerTargetCollector) Collect(ch chan<- prometheus.Metric) {
for _, t := range c.read() {
up := 0.0
if t.Up {
up = 1
}
ch <- prometheus.MustNewConstMetric(c.up, prometheus.GaugeValue, up, t.Provider, t.Target)
ch <- prometheus.MustNewConstMetric(c.duration, prometheus.GaugeValue, t.Duration.Seconds(), t.Provider, t.Target)
ch <- prometheus.MustNewConstMetric(c.checked, prometheus.GaugeValue, float64(t.CheckedAt.UnixNano())/1e9, t.Provider, t.Target)
}
}
+42
View File
@@ -111,3 +111,45 @@ func TestRegister_freshRegistryPerTest(t *testing.T) {
register(t, m1, nil, 0)
register(t, m2, nil, 0)
}
func TestRegisterProviderTargets_scrape(t *testing.T) {
t.Parallel()
checked := time.Unix(1757602800, 0)
targets := []ProviderTarget{
{Provider: "kvm-home", Target: "nas1", Up: true, Duration: 84 * time.Millisecond, CheckedAt: checked},
{Provider: "kvm-home", Target: "nas2", Up: false, Duration: 15 * time.Second, CheckedAt: checked},
}
reg := prometheus.NewRegistry()
if err := RegisterProviderTargets(reg, func() []ProviderTarget { return targets }); err != nil {
t.Fatalf("RegisterProviderTargets: %v", err)
}
expected := `
# HELP proxy_operator_provider_check_duration_seconds Duration of the target's latest provider health check.
# TYPE proxy_operator_provider_check_duration_seconds gauge
proxy_operator_provider_check_duration_seconds{provider="kvm-home",target="nas1"} 0.084
proxy_operator_provider_check_duration_seconds{provider="kvm-home",target="nas2"} 15
# HELP proxy_operator_provider_check_timestamp_seconds Unix time of the target's latest provider health check.
# TYPE proxy_operator_provider_check_timestamp_seconds gauge
proxy_operator_provider_check_timestamp_seconds{provider="kvm-home",target="nas1"} 1.7576028e+09
proxy_operator_provider_check_timestamp_seconds{provider="kvm-home",target="nas2"} 1.7576028e+09
# HELP proxy_operator_provider_target_up Whether the target passed its latest provider health check (1) or not (0).
# TYPE proxy_operator_provider_target_up gauge
proxy_operator_provider_target_up{provider="kvm-home",target="nas1"} 1
proxy_operator_provider_target_up{provider="kvm-home",target="nas2"} 0
`
if err := testutil.GatherAndCompare(reg, strings.NewReader(expected)); err != nil {
t.Error(err)
}
}
func TestRegisterProviderTargets_emptySnapshotHasNoSeries(t *testing.T) {
t.Parallel()
reg := prometheus.NewRegistry()
if err := RegisterProviderTargets(reg, func() []ProviderTarget { return nil }); err != nil {
t.Fatalf("RegisterProviderTargets: %v", err)
}
if n, err := testutil.GatherAndCount(reg); err != nil || n != 0 {
t.Errorf("series before the first check = %d (%v), want 0", n, err)
}
}