Skip to content

Commit a6f0c8a

Browse files
SuperQfishy
authored andcommitted
Add Prometheus metrics to redisbp
Add a Prometheus exporter interface implementation to the redisbp `NewMonitoredClient()`. Signed-off-by: SuperQ <superq@gmail.com>
1 parent 7b6644b commit a6f0c8a

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed

redis/db/redisbp/monitored_client.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/go-redis/redis/v8"
10+
"github.com/prometheus/client_golang/prometheus"
1011
"github.com/reddit/baseplate.go/metricsbp"
1112
)
1213

@@ -25,6 +26,13 @@ type PoolStatser interface {
2526
func NewMonitoredClient(name string, opt *redis.Options) *redis.Client {
2627
client := redis.NewClient(opt)
2728
client.AddHook(SpanHook{ClientName: name})
29+
30+
if err := prometheus.Register(newExporter(client, name)); err != nil {
31+
// prometheus.Register should never fail because
32+
// exporter.Describe is a no-op, but just in case.
33+
return nil
34+
}
35+
2836
return client
2937
}
3038

@@ -33,6 +41,13 @@ func NewMonitoredClient(name string, opt *redis.Options) *redis.Client {
3341
func NewMonitoredFailoverClient(name string, opt *redis.FailoverOptions) *redis.Client {
3442
client := redis.NewFailoverClient(opt)
3543
client.AddHook(SpanHook{ClientName: name})
44+
45+
if err := prometheus.Register(newExporter(client, name)); err != nil {
46+
// prometheus.Register should never fail because
47+
// exporter.Describe is a no-op, but just in case.
48+
return nil
49+
}
50+
3651
return client
3752
}
3853

@@ -78,6 +93,12 @@ func NewMonitoredClusterClient(name string, opt *redis.ClusterOptions) *ClusterC
7893
client := redis.NewClusterClient(opt)
7994
client.AddHook(SpanHook{ClientName: name})
8095

96+
if err := prometheus.Register(newExporter(client, name)); err != nil {
97+
// prometheus.Register should never fail because
98+
// exporter.Describe is a no-op, but just in case.
99+
return nil
100+
}
101+
81102
return &ClusterClient{client}
82103
}
83104

redis/db/redisbp/prometheus.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package redisbp
2+
3+
import (
4+
"github.com/prometheus/client_golang/prometheus"
5+
)
6+
7+
const (
8+
promNamespace = "redisbp"
9+
subsystemPool = "pool"
10+
11+
nameLabel = "pool"
12+
)
13+
14+
// exporter provides an interface for Prometheus metrics.
15+
type exporter struct {
16+
client PoolStatser
17+
name string
18+
19+
poolHitsCounterDesc *prometheus.Desc
20+
poolMissesCounterDesc *prometheus.Desc
21+
poolTimeoutsCounterDesc *prometheus.Desc
22+
totalConnectionsDesc *prometheus.Desc
23+
idleConnectionsDesc *prometheus.Desc
24+
staleConnectionsDesc *prometheus.Desc
25+
}
26+
27+
func newExporter(client PoolStatser, name string) *exporter {
28+
labels := []string{
29+
nameLabel,
30+
}
31+
32+
return &exporter{
33+
client: client,
34+
name: name,
35+
36+
// Upstream docs: https://pkg.go.dev/github.com/go-redis/redis/v8/internal/pool#Stats
37+
38+
// Counters.
39+
poolHitsCounterDesc: prometheus.NewDesc(
40+
prometheus.BuildFQName(promNamespace, subsystemPool, "hits_total"),
41+
"Number of times free connection was found in the pool",
42+
labels,
43+
nil,
44+
),
45+
poolMissesCounterDesc: prometheus.NewDesc(
46+
prometheus.BuildFQName(promNamespace, subsystemPool, "misses_total"),
47+
"Number of times free connection was NOT found in the pool",
48+
labels,
49+
nil,
50+
),
51+
poolTimeoutsCounterDesc: prometheus.NewDesc(
52+
prometheus.BuildFQName(promNamespace, subsystemPool, "timeouts_total"),
53+
"Number of times a wait timeout occurred",
54+
labels,
55+
nil,
56+
),
57+
58+
// Gauges.
59+
totalConnectionsDesc: prometheus.NewDesc(
60+
prometheus.BuildFQName(promNamespace, subsystemPool, "connections"),
61+
"Number of connections in this redisbp pool",
62+
labels,
63+
nil,
64+
),
65+
idleConnectionsDesc: prometheus.NewDesc(
66+
prometheus.BuildFQName(promNamespace, subsystemPool, "idle_connections"),
67+
"Number of idle connections in this redisbp pool",
68+
labels,
69+
nil,
70+
),
71+
staleConnectionsDesc: prometheus.NewDesc(
72+
prometheus.BuildFQName(promNamespace, subsystemPool, "stale_connections"),
73+
"Number of stale connections in this redisbp pool",
74+
labels,
75+
nil,
76+
),
77+
}
78+
}
79+
80+
// Describe implements the prometheus.Collector interface.
81+
func (e *exporter) Describe(ch chan<- *prometheus.Desc) {
82+
// All metrics are described dynamically.
83+
}
84+
85+
// Collect implements prometheus.Collector.
86+
func (e *exporter) Collect(ch chan<- prometheus.Metric) {
87+
stats := e.client.PoolStats()
88+
89+
// Counters.
90+
ch <- prometheus.MustNewConstMetric(
91+
e.poolHitsCounterDesc,
92+
prometheus.CounterValue,
93+
float64(stats.Hits),
94+
e.name,
95+
)
96+
ch <- prometheus.MustNewConstMetric(
97+
e.poolMissesCounterDesc,
98+
prometheus.CounterValue,
99+
float64(stats.Misses),
100+
e.name,
101+
)
102+
ch <- prometheus.MustNewConstMetric(
103+
e.poolTimeoutsCounterDesc,
104+
prometheus.CounterValue,
105+
float64(stats.Timeouts),
106+
e.name,
107+
)
108+
109+
// Gauges.
110+
ch <- prometheus.MustNewConstMetric(
111+
e.totalConnectionsDesc,
112+
prometheus.GaugeValue,
113+
float64(stats.TotalConns),
114+
e.name,
115+
)
116+
ch <- prometheus.MustNewConstMetric(
117+
e.idleConnectionsDesc,
118+
prometheus.GaugeValue,
119+
float64(stats.IdleConns),
120+
e.name,
121+
)
122+
ch <- prometheus.MustNewConstMetric(
123+
e.staleConnectionsDesc,
124+
prometheus.GaugeValue,
125+
float64(stats.StaleConns),
126+
e.name,
127+
)
128+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package redisbp
2+
3+
import (
4+
"sync"
5+
"testing"
6+
7+
"github.com/go-redis/redis/v8"
8+
"github.com/prometheus/client_golang/prometheus"
9+
)
10+
11+
type fakeClient redis.Client
12+
13+
// PoolStats returns connection pool stats.
14+
func (c fakeClient) PoolStats() *redis.PoolStats {
15+
return &redis.PoolStats{
16+
Hits: 1,
17+
Misses: 2,
18+
Timeouts: 3,
19+
20+
TotalConns: 4,
21+
IdleConns: 5,
22+
StaleConns: 6,
23+
}
24+
}
25+
26+
func TestRedisPoolExporter(t *testing.T) {
27+
client := &fakeClient{}
28+
29+
exporter := newExporter(
30+
client,
31+
"test",
32+
)
33+
// No real test here, we just want to make sure that Collect call will not
34+
// panic, which would happen if we have a label mismatch.
35+
ch := make(chan prometheus.Metric)
36+
var wg sync.WaitGroup
37+
wg.Add(1)
38+
go func() {
39+
defer wg.Done()
40+
for range ch {
41+
}
42+
}()
43+
exporter.Collect(ch)
44+
close(ch)
45+
wg.Wait()
46+
}

0 commit comments

Comments
 (0)