github.com/letsencrypt/boulder@v0.20251208.0/redis/metrics.go (about) 1 package redis 2 3 import ( 4 "errors" 5 "slices" 6 "strings" 7 8 "github.com/prometheus/client_golang/prometheus" 9 "github.com/redis/go-redis/v9" 10 ) 11 12 // An interface satisfied by *redis.ClusterClient and also by a mock in our tests. 13 type poolStatGetter interface { 14 PoolStats() *redis.PoolStats 15 } 16 17 var _ poolStatGetter = (*redis.ClusterClient)(nil) 18 19 type metricsCollector struct { 20 statGetter poolStatGetter 21 22 // Stats accessible from the go-redis connector: 23 // https://pkg.go.dev/github.com/go-redis/redis@v6.15.9+incompatible/internal/pool#Stats 24 lookups *prometheus.Desc 25 totalConns *prometheus.Desc 26 idleConns *prometheus.Desc 27 staleConns *prometheus.Desc 28 } 29 30 // Describe is implemented with DescribeByCollect. That's possible because the 31 // Collect method will always return the same metrics with the same descriptors. 32 func (dbc metricsCollector) Describe(ch chan<- *prometheus.Desc) { 33 prometheus.DescribeByCollect(dbc, ch) 34 } 35 36 // Collect first triggers the Redis ClusterClient's PoolStats function. 37 // Then it creates constant metrics for each Stats value on the fly based 38 // on the returned data. 39 // 40 // Note that Collect could be called concurrently, so we depend on PoolStats() 41 // to be concurrency-safe. 42 func (dbc metricsCollector) Collect(ch chan<- prometheus.Metric) { 43 writeGauge := func(stat *prometheus.Desc, val uint32, labelValues ...string) { 44 ch <- prometheus.MustNewConstMetric(stat, prometheus.GaugeValue, float64(val), labelValues...) 45 } 46 47 stats := dbc.statGetter.PoolStats() 48 writeGauge(dbc.lookups, stats.Hits, "hit") 49 writeGauge(dbc.lookups, stats.Misses, "miss") 50 writeGauge(dbc.lookups, stats.Timeouts, "timeout") 51 writeGauge(dbc.totalConns, stats.TotalConns) 52 writeGauge(dbc.idleConns, stats.IdleConns) 53 writeGauge(dbc.staleConns, stats.StaleConns) 54 } 55 56 // newClientMetricsCollector is broken out for testing purposes. 57 func newClientMetricsCollector(statGetter poolStatGetter, labels prometheus.Labels) metricsCollector { 58 return metricsCollector{ 59 statGetter: statGetter, 60 lookups: prometheus.NewDesc( 61 "redis_connection_pool_lookups", 62 "Number of lookups for a connection in the pool, labeled by hit/miss", 63 []string{"result"}, labels), 64 totalConns: prometheus.NewDesc( 65 "redis_connection_pool_total_conns", 66 "Number of total connections in the pool.", 67 nil, labels), 68 idleConns: prometheus.NewDesc( 69 "redis_connection_pool_idle_conns", 70 "Number of idle connections in the pool.", 71 nil, labels), 72 staleConns: prometheus.NewDesc( 73 "redis_connection_pool_stale_conns", 74 "Number of stale connections removed from the pool.", 75 nil, labels), 76 } 77 } 78 79 // MustRegisterClientMetricsCollector registers a metrics collector for the 80 // given Redis client with the provided prometheus.Registerer. The collector 81 // will report metrics labelled by the provided addresses and username. If the 82 // collector is already registered, this function is a no-op. 83 func MustRegisterClientMetricsCollector(client poolStatGetter, stats prometheus.Registerer, addrs map[string]string, user string) { 84 var labelAddrs []string 85 for addr := range addrs { 86 labelAddrs = append(labelAddrs, addr) 87 } 88 // Keep the list of addresses sorted for consistency. 89 slices.Sort(labelAddrs) 90 labels := prometheus.Labels{ 91 "addresses": strings.Join(labelAddrs, ", "), 92 "user": user, 93 } 94 err := stats.Register(newClientMetricsCollector(client, labels)) 95 if err != nil { 96 are := prometheus.AlreadyRegisteredError{} 97 if errors.As(err, &are) { 98 // The collector is already registered using the same labels. 99 return 100 } 101 panic(err) 102 } 103 }