120 lines
4.3 KiB
Go
120 lines
4.3 KiB
Go
// Package metrics defines the operator's Prometheus metrics. Nothing here
|
|
// registers itself — no init(), per house rules — the composition root
|
|
// calls Register explicitly, which also lets every test use a fresh
|
|
// registry.
|
|
package metrics
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
// Metrics holds the vector metrics the operator's components feed. The
|
|
// consuming packages (health, discovery, provider) each define their own
|
|
// small recorder interface; *Metrics satisfies all of them structurally,
|
|
// so none of them import this package's prometheus surface.
|
|
type Metrics struct {
|
|
healthcheckDuration *prometheus.HistogramVec
|
|
healthcheckFailures *prometheus.CounterVec
|
|
leaseRequests *prometheus.CounterVec
|
|
providerRequests *prometheus.CounterVec
|
|
}
|
|
|
|
// New builds the metric set, unregistered.
|
|
func New() *Metrics {
|
|
return &Metrics{
|
|
healthcheckDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
Name: "proxy_operator_healthcheck_duration_seconds",
|
|
Help: "Duration of through-the-proxy health probes.",
|
|
Buckets: prometheus.DefBuckets,
|
|
}, []string{"proxy"}),
|
|
healthcheckFailures: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "proxy_operator_healthcheck_failures_total",
|
|
Help: "Failed health probes.",
|
|
}, []string{"proxy"}),
|
|
leaseRequests: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "proxy_operator_lease_requests_total",
|
|
Help: "Lease acquisition requests by outcome.",
|
|
}, []string{"outcome"}),
|
|
providerRequests: prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "proxy_operator_provider_requests_total",
|
|
Help: "Provider API calls by operation and classified result.",
|
|
}, []string{"provider", "op", "result"}),
|
|
}
|
|
}
|
|
|
|
// Register registers the vectors plus the two scrape-time collectors.
|
|
// proxyPhases and activeLeases are read at every scrape: gauges derived
|
|
// from reconcile-time increments inevitably drift and leak series on
|
|
// delete; reading the source of truth cannot.
|
|
func (m *Metrics) Register(reg prometheus.Registerer, proxyPhases func() map[string]int, activeLeases func() int) error {
|
|
collectors := []prometheus.Collector{
|
|
m.healthcheckDuration,
|
|
m.healthcheckFailures,
|
|
m.leaseRequests,
|
|
m.providerRequests,
|
|
&constCollector{
|
|
desc: prometheus.NewDesc("proxy_operator_proxies",
|
|
"Proxy objects by phase.", []string{"phase"}, nil),
|
|
read: proxyPhases,
|
|
},
|
|
&constCollector{
|
|
desc: prometheus.NewDesc("proxy_operator_leases_active",
|
|
"Currently active leases.", nil, nil),
|
|
read: func() map[string]int { return map[string]int{"": activeLeases()} },
|
|
},
|
|
}
|
|
for _, c := range collectors {
|
|
if err := reg.Register(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ObserveProbe records one health probe. Called on every probe — metrics
|
|
// are the home for high-frequency signal that must never touch status.
|
|
func (m *Metrics) ObserveProbe(proxy string, latency time.Duration, success bool) {
|
|
m.healthcheckDuration.WithLabelValues(proxy).Observe(latency.Seconds())
|
|
if !success {
|
|
m.healthcheckFailures.WithLabelValues(proxy).Inc()
|
|
}
|
|
}
|
|
|
|
// ForgetProxy drops the per-proxy series when the health engine prunes its
|
|
// state — without this, series for deleted proxies leak forever.
|
|
func (m *Metrics) ForgetProxy(proxy string) {
|
|
m.healthcheckDuration.DeleteLabelValues(proxy)
|
|
m.healthcheckFailures.DeleteLabelValues(proxy)
|
|
}
|
|
|
|
// LeaseRequest records a lease acquisition outcome ("granted"|"no_match").
|
|
func (m *Metrics) LeaseRequest(outcome string) {
|
|
m.leaseRequests.WithLabelValues(outcome).Inc()
|
|
}
|
|
|
|
// ProviderRequest records one provider API call with its classified result.
|
|
func (m *Metrics) ProviderRequest(provider, op, result string) {
|
|
m.providerRequests.WithLabelValues(provider, op, result).Inc()
|
|
}
|
|
|
|
// constCollector reads a label→value map at scrape time and emits one
|
|
// gauge sample per entry. An empty-string label key means "no labels".
|
|
type constCollector struct {
|
|
desc *prometheus.Desc
|
|
read func() map[string]int
|
|
}
|
|
|
|
func (c *constCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.desc }
|
|
|
|
func (c *constCollector) Collect(ch chan<- prometheus.Metric) {
|
|
for label, value := range c.read() {
|
|
if label == "" {
|
|
ch <- prometheus.MustNewConstMetric(c.desc, prometheus.GaugeValue, float64(value))
|
|
continue
|
|
}
|
|
ch <- prometheus.MustNewConstMetric(c.desc, prometheus.GaugeValue, float64(value), label)
|
|
}
|
|
}
|