Files
go/extra/redisotel-native/metrics_stress_test.go
ofekshenawaandGitHub cd3949d1a0 fix: connection closure metrics and enable all metric groups by default (#3735)
* fix: connection closure metrics and enable all metric groups by default

This commit addresses three issues in the redisotel-native metrics implementation:

1. Fix connection closure metrics not appearing in dashboards
   - Root cause: CloseConn() in internal/pool/pool.go was not calling
     the metricConnectionClosedCallback when closing stale connections
   - Added callback invocation in CloseConn() to record 'stale' closures

2. Verify tracing and metrics compatibility
   - Added TestTracingAndMetricsCompatibility to document that
     redisotel (tracing via AddHook) and redisotel-native (metrics via
     SetOTelRecorder) can coexist without conflicts

3. Enable all metric groups by default
   - Changed default MetricGroups from connection-basic+resiliency
     to MetricGroupAll for comprehensive observability out of the box
2026-03-16 09:54:24 +02:00

221 lines
5.5 KiB
Go

package redisotel
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
const (
stressTestDuration = 30 * time.Second
stressTestConcurrency = 50
stressTestMinDelay = 10 * time.Millisecond
stressTestMaxDelay = 100 * time.Millisecond
stressTestStatusInterval = 5 * time.Second
)
// TestMetricsUnderStress validates metrics recording under concurrent load.
func TestMetricsUnderStress(t *testing.T) {
ctx := context.Background()
testClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
if err := testClient.Ping(ctx).Err(); err != nil {
testClient.Close()
t.Skip("Redis not available at localhost:6379")
}
testClient.Close()
reader := metric.NewManualReader()
meterProvider := metric.NewMeterProvider(metric.WithReader(reader))
defer func() {
_ = meterProvider.Shutdown(ctx)
}()
otel.SetMeterProvider(meterProvider)
resetObservabilityForTest()
otelInstance := GetObservabilityInstance()
config := NewConfig().
WithEnabled(true).
WithMeterProvider(meterProvider).
WithMetricGroups(MetricGroupAll)
if err := otelInstance.Init(config); err != nil {
t.Fatalf("Failed to initialize OTel: %v", err)
}
defer otelInstance.Shutdown()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: stressTestConcurrency,
MinIdleConns: 10,
})
defer rdb.Close()
var opsCompleted atomic.Int64
var opsErrors atomic.Int64
startTime := time.Now()
deadline := startTime.Add(stressTestDuration)
statusTicker := time.NewTicker(stressTestStatusInterval)
defer statusTicker.Stop()
done := make(chan struct{})
go func() {
for {
select {
case <-statusTicker.C:
elapsed := time.Since(startTime).Seconds()
ops := opsCompleted.Load()
t.Logf("[%.0fs] %d ops, %.1f ops/sec", elapsed, ops, float64(ops)/elapsed)
case <-done:
return
}
}
}()
var wg sync.WaitGroup
for i := 0; i < stressTestConcurrency; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for time.Now().Before(deadline) {
key := fmt.Sprintf("stress_test_key_%d_%d", workerID, rand.Int63())
value := fmt.Sprintf("value_%d", time.Now().UnixNano())
if err := rdb.Set(ctx, key, value, time.Minute).Err(); err != nil {
opsErrors.Add(1)
} else {
opsCompleted.Add(1)
}
if _, err := rdb.Get(ctx, key).Result(); err != nil && err != redis.Nil {
opsErrors.Add(1)
} else {
opsCompleted.Add(1)
}
delay := stressTestMinDelay + time.Duration(rand.Int63n(int64(stressTestMaxDelay-stressTestMinDelay)))
time.Sleep(delay)
}
}(i)
}
wg.Wait()
close(done)
totalOps := opsCompleted.Load()
totalErrors := opsErrors.Load()
elapsed := time.Since(startTime)
t.Logf("Completed: %d ops in %v (%.1f ops/sec), %d errors",
totalOps, elapsed.Round(time.Second), float64(totalOps)/elapsed.Seconds(), totalErrors)
var rm metricdata.ResourceMetrics
if err := reader.Collect(ctx, &rm); err != nil {
t.Fatalf("Failed to collect metrics: %v", err)
}
validateMetrics(t, rm)
}
func validateMetrics(t *testing.T, rm metricdata.ResourceMetrics) {
metricsFound := make(map[string]bool)
for _, sm := range rm.ScopeMetrics {
for _, m := range sm.Metrics {
metricsFound[m.Name] = true
}
}
required := []string{
MetricConnectionCount,
MetricConnectionCreateTime,
MetricOperationDuration,
}
for _, name := range required {
if !metricsFound[name] {
t.Errorf("Required metric not found: %s", name)
}
}
}
func resetObservabilityForTest() {
observabilityInstanceOnce = sync.Once{}
observabilityInstance = nil
}
// TestTracingAndMetricsCompatibility verifies that redisotel (tracing) and
// redisotel-native (metrics) can be used together without conflicts.
// Tracing uses AddHook (per-client), metrics uses SetOTelRecorder (global).
func TestTracingAndMetricsCompatibility(t *testing.T) {
ctx := context.Background()
testClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
if err := testClient.Ping(ctx).Err(); err != nil {
testClient.Close()
t.Skip("Redis not available at localhost:6379")
}
testClient.Close()
reader := metric.NewManualReader()
meterProvider := metric.NewMeterProvider(metric.WithReader(reader))
defer meterProvider.Shutdown(ctx)
otel.SetMeterProvider(meterProvider)
resetObservabilityForTest()
otelInstance := GetObservabilityInstance()
config := NewConfig().
WithEnabled(true).
WithMeterProvider(meterProvider).
WithMetricGroups(MetricGroupAll)
if err := otelInstance.Init(config); err != nil {
t.Fatalf("Failed to initialize OTel metrics: %v", err)
}
defer otelInstance.Shutdown()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 5,
})
defer rdb.Close()
// In production, also call: redisotel.InstrumentTracing(rdb)
for i := 0; i < 10; i++ {
key := fmt.Sprintf("compat-test-%d", i)
if err := rdb.Set(ctx, key, "value", time.Minute).Err(); err != nil {
t.Fatalf("SET failed: %v", err)
}
if _, err := rdb.Get(ctx, key).Result(); err != nil {
t.Fatalf("GET failed: %v", err)
}
}
var rm metricdata.ResourceMetrics
if err := reader.Collect(ctx, &rm); err != nil {
t.Fatalf("Failed to collect metrics: %v", err)
}
found := false
for _, sm := range rm.ScopeMetrics {
for _, m := range sm.Metrics {
if m.Name == MetricOperationDuration {
found = true
}
}
}
if !found {
t.Error("Expected to find db.client.operation.duration metric")
}
}