Skip to content

Commit 2a7725d

Browse files
elena-kolevskaofekshenawa
authored andcommitted
Adds connection state metrics
Signed-off-by: Elena Kolevska <[email protected]>
1 parent d588c3c commit 2a7725d

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

extra/redisotel-native/metrics.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ const (
2121
// metricsRecorder implements the otel.Recorder interface
2222
type metricsRecorder struct {
2323
operationDuration metric.Float64Histogram
24+
connectionCount metric.Int64UpDownCounter
2425

25-
// Client configuration for attributes
26+
// Client configuration for attributes (used for operation metrics only)
2627
serverAddr string
2728
serverPort string
2829
dbIndex string
@@ -267,3 +268,59 @@ func formatDBIndex(db int) string {
267268
}
268269
return strconv.Itoa(db)
269270
}
271+
272+
// RecordConnectionStateChange records a change in connection state
273+
// This is called from the pool when connections transition between states
274+
func (r *metricsRecorder) RecordConnectionStateChange(
275+
ctx context.Context,
276+
cn redis.ConnInfo,
277+
fromState, toState string,
278+
) {
279+
if r.connectionCount == nil {
280+
return
281+
}
282+
283+
// Extract server address from connection
284+
serverAddr, serverPort := extractServerInfo(cn)
285+
286+
// Build base attributes
287+
attrs := []attribute.KeyValue{
288+
attribute.String("db.system", "redis"),
289+
attribute.String("server.address", serverAddr),
290+
}
291+
292+
// Add server.port if not default
293+
if serverPort != "" && serverPort != "6379" {
294+
attrs = append(attrs, attribute.String("server.port", serverPort))
295+
}
296+
297+
// Decrement old state (if not empty)
298+
if fromState != "" {
299+
fromAttrs := append([]attribute.KeyValue{}, attrs...)
300+
fromAttrs = append(fromAttrs, attribute.String("state", fromState))
301+
r.connectionCount.Add(ctx, -1, metric.WithAttributes(fromAttrs...))
302+
}
303+
304+
// Increment new state
305+
if toState != "" {
306+
toAttrs := append([]attribute.KeyValue{}, attrs...)
307+
toAttrs = append(toAttrs, attribute.String("state", toState))
308+
r.connectionCount.Add(ctx, 1, metric.WithAttributes(toAttrs...))
309+
}
310+
}
311+
312+
// extractServerInfo extracts server address and port from connection info
313+
func extractServerInfo(cn redis.ConnInfo) (addr, port string) {
314+
if cn == nil {
315+
return "", ""
316+
}
317+
318+
remoteAddr := cn.RemoteAddr()
319+
if remoteAddr == nil {
320+
return "", ""
321+
}
322+
323+
addrStr := remoteAddr.String()
324+
host, portStr := parseAddr(addrStr)
325+
return host, portStr
326+
}

extra/redisotel-native/redisotel.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,20 @@ func initOnce(client redis.UniversalClient, opts ...Option) error {
125125
return fmt.Errorf("failed to create operation duration histogram: %w", err)
126126
}
127127

128+
// Create synchronous UpDownCounter for connection count
129+
connectionCount, err := meter.Int64UpDownCounter(
130+
"db.client.connection.count",
131+
metric.WithDescription("The number of connections that are currently in state described by the state attribute"),
132+
metric.WithUnit("{connection}"),
133+
)
134+
if err != nil {
135+
return fmt.Errorf("failed to create connection count metric: %w", err)
136+
}
137+
128138
// Create recorder
129139
recorder := &metricsRecorder{
130140
operationDuration: operationDuration,
141+
connectionCount: connectionCount,
131142
serverAddr: serverAddr,
132143
serverPort: serverPort,
133144
dbIndex: dbIndex,

internal/otel/metrics.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ type Cmder interface {
2121
type Recorder interface {
2222
// RecordOperationDuration records the total operation duration (including all retries)
2323
RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, cn *pool.Conn)
24+
25+
// RecordConnectionStateChange records when a connection changes state
26+
RecordConnectionStateChange(ctx context.Context, cn *pool.Conn, fromState, toState string)
2427
}
2528

2629
// Global recorder instance (initialized by extra/redisotel-native)
@@ -30,9 +33,16 @@ var globalRecorder Recorder = noopRecorder{}
3033
func SetGlobalRecorder(r Recorder) {
3134
if r == nil {
3235
globalRecorder = noopRecorder{}
36+
// Unregister pool callback
37+
pool.SetConnectionStateChangeCallback(nil)
3338
return
3439
}
3540
globalRecorder = r
41+
42+
// Register pool callback to forward state changes to recorder
43+
pool.SetConnectionStateChangeCallback(func(ctx context.Context, cn *pool.Conn, fromState, toState string) {
44+
globalRecorder.RecordConnectionStateChange(ctx, cn, fromState, toState)
45+
})
3646
}
3747

3848
// RecordOperationDuration records the total operation duration.
@@ -41,7 +51,14 @@ func RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cm
4151
globalRecorder.RecordOperationDuration(ctx, duration, cmd, attempts, cn)
4252
}
4353

54+
// RecordConnectionStateChange records when a connection changes state.
55+
// This is called from pool.go when connections transition between states.
56+
func RecordConnectionStateChange(ctx context.Context, cn *pool.Conn, fromState, toState string) {
57+
globalRecorder.RecordConnectionStateChange(ctx, cn, fromState, toState)
58+
}
59+
4460
// noopRecorder is a no-op implementation (zero overhead when metrics disabled)
4561
type noopRecorder struct{}
4662

4763
func (noopRecorder) RecordOperationDuration(context.Context, time.Duration, Cmder, int, *pool.Conn) {}
64+
func (noopRecorder) RecordConnectionStateChange(context.Context, *pool.Conn, string, string) {}

internal/pool/pool.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var (
2424
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
2525
ErrPoolTimeout = errors.New("redis: connection pool timeout")
2626

27+
<<<<<<< HEAD
2728
// ErrConnUnusableTimeout is returned when a connection is not usable and we timed out trying to mark it as unusable.
2829
ErrConnUnusableTimeout = errors.New("redis: timed out trying to mark connection as unusable")
2930

@@ -32,6 +33,10 @@ var (
3233

3334
// errConnNotPooled is returned when trying to return a non-pooled connection to the pool.
3435
errConnNotPooled = errors.New("connection not pooled")
36+
=======
37+
// Global callback for connection state changes (set by otel package)
38+
connectionStateChangeCallback func(ctx context.Context, cn *Conn, fromState, toState string)
39+
>>>>>>> c17657c6 (Adds connection state metrics)
3540

3641
// popAttempts is the maximum number of attempts to find a usable connection
3742
// when popping from the idle connection pool. This handles cases where connections
@@ -51,6 +56,23 @@ var (
5156
noExpiration = maxTime
5257
)
5358

59+
<<<<<<< HEAD
60+
=======
61+
// SetConnectionStateChangeCallback sets the global callback for connection state changes.
62+
// This is called by the otel package to register metrics recording.
63+
func SetConnectionStateChangeCallback(fn func(ctx context.Context, cn *Conn, fromState, toState string)) {
64+
connectionStateChangeCallback = fn
65+
}
66+
67+
var timers = sync.Pool{
68+
New: func() interface{} {
69+
t := time.NewTimer(time.Hour)
70+
t.Stop()
71+
return t
72+
},
73+
}
74+
75+
>>>>>>> c17657c6 (Adds connection state metrics)
5476
// Stats contains pool state information and accumulated stats.
5577
type Stats struct {
5678
Hits uint32 // number of times free connection was found in the pool
@@ -524,6 +546,12 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
524546
}
525547

526548
atomic.AddUint32(&p.stats.Hits, 1)
549+
550+
// Notify metrics: connection moved from idle to used
551+
if connectionStateChangeCallback != nil {
552+
connectionStateChangeCallback(ctx, cn, "idle", "used")
553+
}
554+
527555
return cn, nil
528556
}
529557

@@ -546,6 +574,12 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
546574
return nil, err
547575
}
548576
}
577+
578+
// Notify metrics: new connection is created and used
579+
if connectionStateChangeCallback != nil {
580+
connectionStateChangeCallback(ctx, newcn, "", "used")
581+
}
582+
549583
return newcn, nil
550584
}
551585

@@ -840,9 +874,26 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
840874
p.connsMu.Unlock()
841875
p.idleConnsLen.Add(1)
842876
}
877+
<<<<<<< HEAD
878+
=======
879+
p.idleConnsLen.Add(1)
880+
881+
// Notify metrics: connection moved from used to idle
882+
if connectionStateChangeCallback != nil {
883+
connectionStateChangeCallback(ctx, cn, "used", "idle")
884+
}
885+
>>>>>>> c17657c6 (Adds connection state metrics)
843886
} else {
844887
shouldCloseConn = true
888+
<<<<<<< HEAD
845889
p.removeConnWithLock(cn)
890+
=======
891+
892+
// Notify metrics: connection removed (used -> nothing)
893+
if connectionStateChangeCallback != nil {
894+
connectionStateChangeCallback(ctx, cn, "used", "")
895+
}
896+
>>>>>>> c17657c6 (Adds connection state metrics)
846897
}
847898

848899
if freeTurn {
@@ -857,6 +908,7 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
857908
}
858909

859910
func (p *ConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
911+
<<<<<<< HEAD
860912
p.removeConnInternal(ctx, cn, reason, true)
861913
}
862914

@@ -877,12 +929,19 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro
877929
hookManager.ProcessOnRemove(ctx, cn, reason)
878930
}
879931

932+
=======
933+
>>>>>>> c17657c6 (Adds connection state metrics)
880934
p.removeConnWithLock(cn)
881935

882936
if freeTurn {
883937
p.freeTurn()
884938
}
885939

940+
// Notify metrics: connection removed (assume from used state)
941+
if connectionStateChangeCallback != nil {
942+
connectionStateChangeCallback(ctx, cn, "used", "")
943+
}
944+
886945
_ = p.closeConn(cn)
887946

888947
// Check if we need to create new idle connections to maintain MinIdleConns

otel.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ type ConnInfo interface {
2424
type OTelRecorder interface {
2525
// RecordOperationDuration records the total operation duration (including all retries)
2626
RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, cn ConnInfo)
27+
28+
// RecordConnectionStateChange records when a connection changes state (e.g., idle -> used)
29+
RecordConnectionStateChange(ctx context.Context, cn ConnInfo, fromState, toState string)
2730
}
2831

2932
// SetOTelRecorder sets the global OpenTelemetry recorder.
@@ -55,3 +58,12 @@ func (a *otelRecorderAdapter) RecordOperationDuration(ctx context.Context, durat
5558
}
5659
}
5760

61+
func (a *otelRecorderAdapter) RecordConnectionStateChange(ctx context.Context, cn *pool.Conn, fromState, toState string) {
62+
// Convert internal pool.Conn to public ConnInfo
63+
var connInfo ConnInfo
64+
if cn != nil {
65+
connInfo = cn
66+
}
67+
a.recorder.RecordConnectionStateChange(ctx, connInfo, fromState, toState)
68+
}
69+

0 commit comments

Comments
 (0)