package redisprom import ( "context" "time" "github.com/go-redis/redis/v8" "github.com/prometheus/client_golang/prometheus" ) type ( // Hook represents a go-redis hook that exports metrics of commands and pipelines. // // The following metrics are exported: // // - Single commands (not-pipelined) // - Histogram of duration // - Counter of errors // // - Pipelined commands // - Counter of commands // - Counter of errors // // The duration of individual pipelined commands won't be collected, but the overall duration of the // pipeline will, with a pseudo-command called "pipeline". Hook struct { options *Options singleCommands *prometheus.HistogramVec pipelinedCommands *prometheus.CounterVec singleErrors *prometheus.CounterVec pipelinedErrors *prometheus.CounterVec } startKey struct{} ) var ( labelNames = []string{"instance", "command"} ) // NewHook creates a new go-redis hook instance and registers Prometheus collectors. func NewHook(opts ...Option) *Hook { options := DefaultOptions() options.Merge(opts...) singleCommands := register(prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: options.Namespace, Name: "redis_single_commands", Help: "Histogram of single Redis commands", Buckets: options.DurationBuckets, }, labelNames)).(*prometheus.HistogramVec) pipelinedCommands := register(prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: options.Namespace, Name: "redis_pipelined_commands", Help: "Number of pipelined Redis commands", }, labelNames)).(*prometheus.CounterVec) singleErrors := register(prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: options.Namespace, Name: "redis_single_errors", Help: "Number of single Redis commands that have failed", }, labelNames)).(*prometheus.CounterVec) pipelinedErrors := register(prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: options.Namespace, Name: "redis_pipelined_errors", Help: "Number of pipelined Redis commands that have failed", }, labelNames)).(*prometheus.CounterVec) return &Hook{ options: options, singleCommands: singleCommands, pipelinedCommands: pipelinedCommands, singleErrors: singleErrors, pipelinedErrors: pipelinedErrors, } } func (hook *Hook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { return context.WithValue(ctx, startKey{}, time.Now()), nil } func (hook *Hook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { if start, ok := ctx.Value(startKey{}).(time.Time); ok { duration := time.Since(start).Seconds() hook.singleCommands.WithLabelValues(hook.options.InstanceName, cmd.Name()).Observe(duration) } if isActualErr(cmd.Err()) { hook.singleErrors.WithLabelValues(hook.options.InstanceName, cmd.Name()).Inc() } return nil } func (hook *Hook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { return context.WithValue(ctx, startKey{}, time.Now()), nil } func (hook *Hook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { if err := hook.AfterProcess(ctx, redis.NewCmd(ctx, "pipeline")); err != nil { return err } for _, cmd := range cmds { hook.pipelinedCommands.WithLabelValues(hook.options.InstanceName, cmd.Name()).Inc() if isActualErr(cmd.Err()) { hook.pipelinedErrors.WithLabelValues(hook.options.InstanceName, cmd.Name()).Inc() } } return nil } func register(collector prometheus.Collector) prometheus.Collector { err := prometheus.DefaultRegisterer.Register(collector) if err == nil { return collector } if arErr, ok := err.(prometheus.AlreadyRegisteredError); ok { return arErr.ExistingCollector } panic(err) } func isActualErr(err error) bool { return err != nil && err != redis.Nil }