diff --git a/go.mod b/go.mod index 2bdb4e4..ab2bb80 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( connectrpc.com/connect v1.19.1 github.com/alicebob/miniredis/v2 v2.37.0 github.com/evalops/proto v0.0.0-20260414000509-37b2bf5d6244 - github.com/evalops/service-runtime v0.1.17 + github.com/evalops/service-runtime v0.1.18 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jackc/pgx/v5 v5.9.1 github.com/pashagolub/pgxmock/v4 v4.9.0 diff --git a/go.sum b/go.sum index db7d11b..d379e9c 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/evalops/proto v0.0.0-20260414000509-37b2bf5d6244 h1:MjoKPIxG/OisQLDqLvbvpleCAkPgr2InTrK4Q0D9F1o= github.com/evalops/proto v0.0.0-20260414000509-37b2bf5d6244/go.mod h1:EXB8IcqMaV58Tt0w2GaQia3YOwzrEvnIWFZetHX+IxE= -github.com/evalops/service-runtime v0.1.17 h1:Elnn2Z+qLoxweauNikJQnQKt/0smEEdzOc0qQCqxo5g= -github.com/evalops/service-runtime v0.1.17/go.mod h1:ZAB9GcnbFaFUq+X5TsoMUUth6KnAVR6uVHMKanXtQTY= +github.com/evalops/service-runtime v0.1.18 h1:xu/MgtmcRDbUP9QkbX862nQLsT/nFAEyM7AIyVA7Mlk= +github.com/evalops/service-runtime v0.1.18/go.mod h1:ZAB9GcnbFaFUq+X5TsoMUUth6KnAVR6uVHMKanXtQTY= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= diff --git a/internal/bootstrap/redis_observability.go b/internal/bootstrap/redis_observability.go new file mode 100644 index 0000000..8de1855 --- /dev/null +++ b/internal/bootstrap/redis_observability.go @@ -0,0 +1,25 @@ +package bootstrap + +import ( + "github.com/evalops/service-runtime/observability" + "github.com/prometheus/client_golang/prometheus" + goredis "github.com/redis/go-redis/v9" +) + +func instrumentDefaultRedisClient(client goredis.UniversalClient) error { + return instrumentRedisClient(client, prometheus.DefaultRegisterer) +} + +func instrumentRedisClient(client goredis.UniversalClient, registerer prometheus.Registerer) error { + if client == nil { + return nil + } + hook, err := observability.NewRedisCommandHook("asb", observability.RedisCommandMetricsOptions{ + Registerer: registerer, + }) + if err != nil { + return err + } + client.AddHook(hook) + return nil +} diff --git a/internal/bootstrap/redis_observability_test.go b/internal/bootstrap/redis_observability_test.go new file mode 100644 index 0000000..8bb3302 --- /dev/null +++ b/internal/bootstrap/redis_observability_test.go @@ -0,0 +1,52 @@ +package bootstrap + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + miniredis "github.com/alicebob/miniredis/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + goredis "github.com/redis/go-redis/v9" +) + +func TestInstrumentRedisClientRecordsCommandMetrics(t *testing.T) { + t.Parallel() + + server, err := miniredis.Run() + if err != nil { + t.Fatalf("miniredis.Run() error = %v", err) + } + defer server.Close() + + registry := prometheus.NewRegistry() + client := goredis.NewClient(&goredis.Options{Addr: server.Addr()}) + defer func() { _ = client.Close() }() + + if err := instrumentRedisClient(client, registry); err != nil { + t.Fatalf("instrumentRedisClient() error = %v", err) + } + if err := client.Set(context.Background(), "relay:test", "ok", 0).Err(); err != nil { + t.Fatalf("Set() error = %v", err) + } + if err := client.Get(context.Background(), "relay:test").Err(); err != nil { + t.Fatalf("Get() error = %v", err) + } + + recorder := httptest.NewRecorder() + promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/metrics", nil)) + + body := recorder.Body.String() + if !strings.Contains(body, `asb_redis_commands_total{command="set",status="ok"} 1`) { + t.Fatalf("metrics body = %q, want Redis set counter", body) + } + if !strings.Contains(body, `asb_redis_commands_total{command="get",status="ok"} 1`) { + t.Fatalf("metrics body = %q, want Redis get counter", body) + } + if !strings.Contains(body, `asb_redis_command_duration_seconds_count{command="set",status="ok"} 1`) { + t.Fatalf("metrics body = %q, want Redis set duration histogram", body) + } +} diff --git a/internal/bootstrap/service.go b/internal/bootstrap/service.go index 94d90cc..4384eab 100644 --- a/internal/bootstrap/service.go +++ b/internal/bootstrap/service.go @@ -469,6 +469,9 @@ func newRuntimeStore(ctx context.Context) (core.RuntimeStore, func(), readinessP Password: os.Getenv("ASB_REDIS_PASSWORD"), DB: 0, }) + if err := instrumentDefaultRedisClient(client); err != nil { + return nil, nil, nil, nil, err + } if err := client.Ping(ctx).Err(); err != nil { return nil, nil, nil, nil, err }