feat(v2/redis): add instrumented Redis client
Parent Issue: gitlab-org/quality/quality-engineering/team-tasks#4455 (closed)
Preamble:
This implementation grew to be larger than I had initially expected. Apologies to my reviewers; I plan on iterating with smaller MRs in the future.
I assume that this will require some back and forth to flesh out the exact requirements. I hope this is an acceptable first step.
Summary:
Introduces gitlab.com/gitlab-org/labkit/v2/redis, an instrumented Redis client that satisfies app.Component and supports single-node, Sentinel, and Cluster topologies through a single unified Config. Instrumentation (OTel tracing, Prometheus metrics, structured logging) is applied as hooks at construction time and requires no changes at call sites.
The package wraps https://github.com/redis/go-redis and exposes goredis.UniversalClient directly via Client.Redis(), giving callers full access to the go-redis API without any adapter layer.
Key Things to Look Out For:
- 98% of my manual testing effort has been focused on the single node Redis Client. Thought there are integration tests for Sentinal + Cluster clients.
- I've set up a local in-process Redis stub for use in unit testing
v2/testing/redistest/redistest.go) goredis.UniversalOptionsis a very large struct. We can either expose this directly during client initialization or wrap the required fields. I've elected to wrap a subset of the options (19 of 49) that I expected to be most commonly used or are required to differentiate which client type should be created. Would love some feedback on if this is a good approach or not.v2/redis/integration_test.goincludes instructions and integration tests to be run alongside Redis (available as Docker Compose services inv2/redis/testdata/). I'll hook these up in CI as a follow up action.- Metrics are registered in
v2/redis/pool_collector.goandv2/redis/metrics_hook.go. These live in separate files because the pool metrics are pulled and are not part of the hook, which is responsible only for the per-command metrics.
Prometheus Metrics
When a prometheus.Registerer is provided, individual command and pool metrics are automatically registered.
| Metric | Type | Labels | Description |
|---|---|---|---|
gitlab_redis_command_duration_seconds |
Histogram | name, command |
Duration of each Redis command. command="pipeline" for pipelines. Uses prometheus.DefBuckets. |
gitlab_redis_command_errors_total |
Counter | name, command |
Commands that returned an error. redis.Nil is excluded. |
gitlab_redis_pool_hit_total |
Counter | name |
Pool connections found without waiting. |
gitlab_redis_pool_miss_total |
Counter | name |
Pool misses — a new connection was established. |
gitlab_redis_pool_timeout_total |
Counter | name |
Requests that timed out waiting for a pool connection. |
gitlab_redis_pool_conn_total_current |
Gauge | name |
Current total connections in the pool. |
gitlab_redis_pool_conn_idle_current |
Gauge | name |
Current idle connections in the pool. |
gitlab_redis_pool_conn_stale_total |
Counter | name |
Connections closed because they exceeded idle/lifetime limits. |
Example Usage:
Note
Start a local Redis server before running the example usage: docker-compose -f ./v2/redis/testdata/docker-compose.yml up -d
func main() {
ctx := context.Background()
// METRICS
m, _ := metrics.New()
// LOGGER
logger := log.New()
// REDIS CLIENT
client, err := redis.NewWithConfig(&redis.Config{
Addrs: []string{"localhost:6379"}, # Locally running Redis instance
Name: "lhollinda-instance",
Registerer: m.Registerer(),
Logger: logger,
})
if err != nil {
panic(err)
}
_ = client.Start(ctx)
defer client.Shutdown(ctx)
// DEMO REDIS COMMANDS
_, _ = client.Redis().SetEx(ctx, "testKey", "testValue", 100*time.Second).Result()
_, _ = client.Redis().GetEx(ctx, "testKey", 1*time.Second).Result()
// SERVE METRICS
http.Handle("/metrics", m.Handler())
}Resulting logs:
- With some manually added comments
- Started with
GITLAB_LOG_LEVEL=debug go run main.go
### go-redis connecting to the server
{"time":"2026-05-20T21:47:49.122718Z","level":"DEBUG","msg":"redis command","name":"lhollinda-instance","redis_command":"hello","redis_duration_ms":0.970417,"redis_key":"3"}
{"time":"2026-05-20T21:47:49.123385Z","level":"DEBUG","msg":"redis pipeline","name":"lhollinda-instance","redis_command":"pipeline","redis_pipeline_size":1,"redis_duration_ms":0.5575000000000001}
{"time":"2026-05-20T21:47:49.123893Z","level":"WARN","msg":"redis command","name":"lhollinda-instance","redis_command":"client","redis_duration_ms":0.492667,"redis_key":"maint_notifications","error_message":"ERR unknown subcommand 'maint_notifications'. Try CLIENT HELP."}
{"time":"2026-05-20T21:47:49.124401Z","level":"DEBUG","msg":"redis pipeline","name":"lhollinda-instance","redis_command":"pipeline","redis_pipeline_size":2,"redis_duration_ms":0.481459}
### Ping Command from Client.Start()
{"time":"2026-05-20T21:47:49.124886Z","level":"DEBUG","msg":"redis command","name":"lhollinda-instance","redis_command":"ping","redis_duration_ms":4.4850840000000005}
{"time":"2026-05-20T21:47:49.124896Z","level":"INFO","msg":"redis client connected","name":"lhollinda-instance"}
### Set + Get Commands
{"time":"2026-05-20T21:47:49.125442Z","level":"DEBUG","msg":"redis command","name":"lhollinda-instance","redis_command":"setex","redis_duration_ms":0.49474999999999997,"redis_key":"testKey"}
{"time":"2026-05-20T21:47:49.126005Z","level":"DEBUG","msg":"redis command","name":"lhollinda-instance","redis_command":"getex","redis_duration_ms":0.518208,"redis_key":"testKey"}Resulting metrics:
> curl localhost:2112/metrics
# HELP gitlab_redis_command_duration_seconds Duration of Redis commands in seconds.
# TYPE gitlab_redis_command_duration_seconds histogram
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.005"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.01"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.025"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.05"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.1"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.25"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="0.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="1"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="2.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="5"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="10"} 1
gitlab_redis_command_duration_seconds_bucket{command="client",name="lhollinda-instance",le="+Inf"} 1
gitlab_redis_command_duration_seconds_sum{command="client",name="lhollinda-instance"} 0.000335625
gitlab_redis_command_duration_seconds_count{command="client",name="lhollinda-instance"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.005"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.01"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.025"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.05"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.1"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.25"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="0.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="1"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="2.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="5"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="10"} 1
gitlab_redis_command_duration_seconds_bucket{command="getex",name="lhollinda-instance",le="+Inf"} 1
gitlab_redis_command_duration_seconds_sum{command="getex",name="lhollinda-instance"} 0.000330458
gitlab_redis_command_duration_seconds_count{command="getex",name="lhollinda-instance"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.005"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.01"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.025"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.05"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.1"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.25"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="0.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="1"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="2.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="5"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="10"} 1
gitlab_redis_command_duration_seconds_bucket{command="hello",name="lhollinda-instance",le="+Inf"} 1
gitlab_redis_command_duration_seconds_sum{command="hello",name="lhollinda-instance"} 0.000769333
gitlab_redis_command_duration_seconds_count{command="hello",name="lhollinda-instance"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.005"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.01"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.025"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.05"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.1"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.25"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="0.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="1"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="2.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="5"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="10"} 1
gitlab_redis_command_duration_seconds_bucket{command="ping",name="lhollinda-instance",le="+Inf"} 1
gitlab_redis_command_duration_seconds_sum{command="ping",name="lhollinda-instance"} 0.002563625
gitlab_redis_command_duration_seconds_count{command="ping",name="lhollinda-instance"} 1
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.005"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.01"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.025"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.05"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.1"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.25"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="0.5"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="1"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="2.5"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="5"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="10"} 2
gitlab_redis_command_duration_seconds_bucket{command="pipeline",name="lhollinda-instance",le="+Inf"} 2
gitlab_redis_command_duration_seconds_sum{command="pipeline",name="lhollinda-instance"} 0.0006498750000000001
gitlab_redis_command_duration_seconds_count{command="pipeline",name="lhollinda-instance"} 2
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.005"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.01"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.025"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.05"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.1"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.25"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="0.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="1"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="2.5"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="5"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="10"} 1
gitlab_redis_command_duration_seconds_bucket{command="setex",name="lhollinda-instance",le="+Inf"} 1
gitlab_redis_command_duration_seconds_sum{command="setex",name="lhollinda-instance"} 0.000307583
gitlab_redis_command_duration_seconds_count{command="setex",name="lhollinda-instance"} 1
# HELP gitlab_redis_command_errors_total Total number of Redis command errors, excluding redis.Nil.
# TYPE gitlab_redis_command_errors_total counter
gitlab_redis_command_errors_total{command="client",name="lhollinda-instance"} 1
# HELP gitlab_redis_pool_conn_idle_current Current number of idle connections in the pool.
# TYPE gitlab_redis_pool_conn_idle_current gauge
gitlab_redis_pool_conn_idle_current{name="lhollinda-instance"} 1
# HELP gitlab_redis_pool_conn_stale_total Number of connections removed from the pool because they were stale.
# TYPE gitlab_redis_pool_conn_stale_total counter
gitlab_redis_pool_conn_stale_total{name="lhollinda-instance"} 0
# HELP gitlab_redis_pool_conn_total_current Current number of connections in the pool.
# TYPE gitlab_redis_pool_conn_total_current gauge
gitlab_redis_pool_conn_total_current{name="lhollinda-instance"} 1
# HELP gitlab_redis_pool_hit_total Number of times a connection was found in the pool.
# TYPE gitlab_redis_pool_hit_total counter
gitlab_redis_pool_hit_total{name="lhollinda-instance"} 2
# HELP gitlab_redis_pool_miss_total Number of times a connection was not found in the pool.
# TYPE gitlab_redis_pool_miss_total counter
gitlab_redis_pool_miss_total{name="lhollinda-instance"} 1
# HELP gitlab_redis_pool_timeout_total Number of times a timeout occurred when looking for a connection in the pool.
# TYPE gitlab_redis_pool_timeout_total counter
gitlab_redis_pool_timeout_total{name="lhollinda-instance"} 0
# HELP promhttp_metric_handler_errors_total Total number of internal errors encountered by the promhttp metric handler.
# TYPE promhttp_metric_handler_errors_total counter
promhttp_metric_handler_errors_total{cause="encoding"} 0
promhttp_metric_handler_errors_total{cause="gathering"} 0