1
0
mirror of https://github.com/redis/go-redis.git synced 2025-08-01 16:06:54 +03:00

feat: add OpenTelemetry metrics instrumentation

This commit is contained in:
Vladimir Mihailenco
2022-10-11 15:37:34 +03:00
parent 41a533cb7b
commit 0dff3d1461
25 changed files with 1152 additions and 737 deletions

128
extra/redisotel/config.go Normal file
View File

@ -0,0 +1,128 @@
package redisotel
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
type config struct {
// Common options.
attrs []attribute.KeyValue
// Tracing options.
tp trace.TracerProvider
tracer trace.Tracer
dbStmtEnabled bool
// Metrics options.
mp metric.MeterProvider
meter metric.Meter
poolName string
}
type baseOption interface {
apply(conf *config)
}
type Option interface {
baseOption
tracing()
metrics()
}
type option func(conf *config)
func (fn option) apply(conf *config) {
fn(conf)
}
func (fn option) tracing() {}
func (fn option) metrics() {}
func newConfig(opts ...baseOption) *config {
conf := &config{
tp: otel.GetTracerProvider(),
mp: global.MeterProvider(),
attrs: []attribute.KeyValue{
semconv.DBSystemRedis,
},
dbStmtEnabled: true,
}
for _, opt := range opts {
opt.apply(conf)
}
return conf
}
// WithAttributes specifies additional attributes to be added to the span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return option(func(conf *config) {
conf.attrs = append(conf.attrs, attrs...)
})
}
//------------------------------------------------------------------------------
type TracingOption interface {
baseOption
tracing()
}
type tracingOption func(conf *config)
var _ TracingOption = (*tracingOption)(nil)
func (fn tracingOption) apply(conf *config) {
fn(conf)
}
func (fn tracingOption) tracing() {}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
// If none is specified, the global provider is used.
func WithTracerProvider(provider trace.TracerProvider) TracingOption {
return tracingOption(func(conf *config) {
conf.tp = provider
})
}
// WithDBStatement tells the tracing hook not to log raw redis commands.
func WithDBStatement(on bool) TracingOption {
return tracingOption(func(conf *config) {
conf.dbStmtEnabled = on
})
}
//------------------------------------------------------------------------------
type MetricsOption interface {
baseOption
metrics()
}
type metricsOption func(conf *config)
var _ MetricsOption = (*metricsOption)(nil)
func (fn metricsOption) apply(conf *config) {
fn(conf)
}
func (fn metricsOption) metrics() {}
// WithMeterProvider configures a metric.Meter used to create instruments.
func WithMeterProvider(mp metric.MeterProvider) MetricsOption {
return metricsOption(func(conf *config) {
conf.mp = mp
})
}

View File

@ -9,7 +9,8 @@ replace github.com/go-redis/redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/go-redis/redis/extra/rediscmd/v9 v9.0.0-beta.2
github.com/go-redis/redis/v9 v9.0.0-beta.2
go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel v1.10.0
go.opentelemetry.io/otel/metric v0.32.1
go.opentelemetry.io/otel/sdk v1.9.0
go.opentelemetry.io/otel/trace v1.9.0
go.opentelemetry.io/otel/trace v1.10.0
)

View File

@ -67,12 +67,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel v1.10.0 h1:Y7DTJMR6zs1xkS/upamJYk0SxxN4C9AqRd77jmZnyY4=
go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ=
go.opentelemetry.io/otel/metric v0.32.1 h1:ftff5LSBCIDwL0UkhBuDg8j9NNxx2IusvJ18q9h6RC4=
go.opentelemetry.io/otel/metric v0.32.1/go.mod h1:iLPP7FaKMAD5BIxJ2VX7f2KTuz//0QK2hEUyti5psqQ=
go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc=
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E=
go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

194
extra/redisotel/metrics.go Normal file
View File

@ -0,0 +1,194 @@
package redisotel
import (
"context"
"fmt"
"net"
"time"
"github.com/go-redis/redis/v9"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
)
// InstrumentMetrics starts reporting OpenTelemetry Metrics.
//
// Based on https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/database-metrics.md
func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error {
baseOpts := make([]baseOption, len(opts))
for i, opt := range opts {
baseOpts[i] = opt
}
conf := newConfig(baseOpts...)
if conf.meter == nil {
conf.meter = conf.mp.Meter(
instrumName,
metric.WithInstrumentationVersion("semver:"+redis.Version()),
)
}
switch rdb := rdb.(type) {
case *redis.Client:
if conf.poolName == "" {
opt := rdb.Options()
conf.poolName = opt.Addr
}
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
if err := reportPoolStats(rdb, conf); err != nil {
return err
}
if err := addMetricsHook(rdb, conf); err != nil {
return err
}
return nil
case *redis.ClusterClient:
rdb.OnNewNode(func(rdb *redis.Client) {
if conf.poolName == "" {
opt := rdb.Options()
conf.poolName = opt.Addr
}
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
if err := reportPoolStats(rdb, conf); err != nil {
otel.Handle(err)
}
if err := addMetricsHook(rdb, conf); err != nil {
otel.Handle(err)
}
})
return nil
case *redis.Ring:
rdb.OnNewNode(func(rdb *redis.Client) {
if err := reportPoolStats(rdb, conf); err != nil {
otel.Handle(err)
}
if err := addMetricsHook(rdb, conf); err != nil {
otel.Handle(err)
}
})
return nil
default:
return fmt.Errorf("redisotel: %T not supported", rdb)
}
}
func reportPoolStats(rdb *redis.Client, conf *config) error {
labels := conf.attrs
idleAttrs := append(labels, attribute.String("state", "idle"))
usedAttrs := append(labels, attribute.String("state", "used"))
usage, err := conf.meter.AsyncInt64().UpDownCounter(
"db.client.connections.usage",
instrument.WithDescription("The number of connections that are currently in state described by the state attribute"),
)
if err != nil {
return err
}
timeouts, err := conf.meter.AsyncInt64().UpDownCounter(
"db.client.connections.timeouts",
instrument.WithDescription("The number of connection timeouts that have occurred trying to obtain a connection from the pool"),
)
if err != nil {
return err
}
return conf.meter.RegisterCallback(
[]instrument.Asynchronous{
usage,
timeouts,
},
func(ctx context.Context) {
stats := rdb.PoolStats()
usage.Observe(ctx, int64(stats.IdleConns), idleAttrs...)
usage.Observe(ctx, int64(stats.TotalConns-stats.IdleConns), usedAttrs...)
timeouts.Observe(ctx, int64(stats.Timeouts), labels...)
},
)
}
func addMetricsHook(rdb *redis.Client, conf *config) error {
createTime, err := conf.meter.SyncInt64().Histogram(
"db.client.connections.create_time",
instrument.WithDescription("The time it took to create a new connection."),
instrument.WithUnit("ms"),
)
if err != nil {
return err
}
useTime, err := conf.meter.SyncInt64().Histogram(
"db.client.connections.use_time",
instrument.WithDescription("The time between borrowing a connection and returning it to the pool."),
instrument.WithUnit("ms"),
)
if err != nil {
return err
}
rdb.AddHook(&metricsHook{
createTime: createTime,
useTime: useTime,
})
return nil
}
type metricsHook struct {
createTime syncint64.Histogram
useTime syncint64.Histogram
}
var _ redis.Hook = (*metricsHook)(nil)
func (mh *metricsHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
start := time.Now()
conn, err := hook(ctx, network, addr)
mh.createTime.Record(ctx, time.Since(start).Milliseconds())
return conn, err
}
}
func (mh *metricsHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
start := time.Now()
err := hook(ctx, cmd)
dur := time.Since(start).Milliseconds()
mh.useTime.Record(ctx, dur, attribute.String("type", "command"), statusAttr(err))
return err
}
}
func (mh *metricsHook) ProcessPipelineHook(
hook redis.ProcessPipelineHook,
) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
start := time.Now()
err := hook(ctx, cmds)
dur := time.Since(start).Milliseconds()
mh.useTime.Record(ctx, dur, attribute.String("type", "pipeline"), statusAttr(err))
return err
}
}
func statusAttr(err error) attribute.KeyValue {
if err != nil {
return attribute.String("status", "error")
}
return attribute.String("status", "ok")
}

View File

@ -1,153 +0,0 @@
package redisotel
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"go.opentelemetry.io/otel/trace"
"github.com/go-redis/redis/extra/rediscmd/v9"
"github.com/go-redis/redis/v9"
)
const (
defaultTracerName = "github.com/go-redis/redis/extra/redisotel"
)
type TracingHook struct {
tracer trace.Tracer
attrs []attribute.KeyValue
dbStmtEnabled bool
}
func NewTracingHook(opts ...Option) *TracingHook {
cfg := &config{
tp: otel.GetTracerProvider(),
attrs: []attribute.KeyValue{
semconv.DBSystemRedis,
},
dbStmtEnabled: true,
}
for _, opt := range opts {
opt.apply(cfg)
}
tracer := cfg.tp.Tracer(
defaultTracerName,
trace.WithInstrumentationVersion("semver:"+redis.Version()),
)
return &TracingHook{tracer: tracer, attrs: cfg.attrs, dbStmtEnabled: cfg.dbStmtEnabled}
}
func (th *TracingHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
if !trace.SpanFromContext(ctx).IsRecording() {
return ctx, nil
}
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(th.attrs...),
}
if th.dbStmtEnabled {
opts = append(opts, trace.WithAttributes(semconv.DBStatementKey.String(rediscmd.CmdString(cmd))))
}
ctx, _ = th.tracer.Start(ctx, cmd.FullName(), opts...)
return ctx, nil
}
func (th *TracingHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
span := trace.SpanFromContext(ctx)
if err := cmd.Err(); err != nil {
recordError(ctx, span, err)
}
span.End()
return nil
}
func (th *TracingHook) BeforeProcessPipeline(
ctx context.Context, cmds []redis.Cmder,
) (context.Context, error) {
if !trace.SpanFromContext(ctx).IsRecording() {
return ctx, nil
}
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(th.attrs...),
trace.WithAttributes(
attribute.Int("db.redis.num_cmd", len(cmds)),
),
}
summary, cmdsString := rediscmd.CmdsString(cmds)
if th.dbStmtEnabled {
opts = append(opts, trace.WithAttributes(semconv.DBStatementKey.String(cmdsString)))
}
ctx, _ = th.tracer.Start(ctx, "pipeline "+summary, opts...)
return ctx, nil
}
func (th *TracingHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {
span := trace.SpanFromContext(ctx)
if err := cmds[0].Err(); err != nil {
recordError(ctx, span, err)
}
span.End()
return nil
}
func recordError(ctx context.Context, span trace.Span, err error) {
if err != redis.Nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
}
type config struct {
tp trace.TracerProvider
attrs []attribute.KeyValue
dbStmtEnabled bool
}
// Option specifies instrumentation configuration options.
type Option interface {
apply(*config)
}
type optionFunc func(*config)
func (o optionFunc) apply(c *config) {
o(c)
}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
// If none is specified, the global provider is used.
func WithTracerProvider(provider trace.TracerProvider) Option {
return optionFunc(func(cfg *config) {
if provider != nil {
cfg.tp = provider
}
})
}
// WithAttributes specifies additional attributes to be added to the span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return optionFunc(func(cfg *config) {
cfg.attrs = append(cfg.attrs, attrs...)
})
}
// WithDBStatement tells the tracing hook not to log raw redis commands.
func WithDBStatement(on bool) Option {
return optionFunc(func(cfg *config) {
cfg.dbStmtEnabled = on
})
}

View File

@ -1,4 +1,4 @@
package redisotel_test
package redisotel
import (
"context"
@ -10,16 +10,9 @@ import (
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/go-redis/redis/extra/redisotel/v9"
"github.com/go-redis/redis/v9"
)
func TestNew(t *testing.T) {
// this also functions as a compile-time test that the
// TracingHook conforms to the Hook interface
var _ redis.Hook = redisotel.NewTracingHook()
}
type providerFunc func(name string, opts ...trace.TracerOption) trace.Tracer
func (fn providerFunc) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
@ -34,7 +27,7 @@ func TestNewWithTracerProvider(t *testing.T) {
return otel.GetTracerProvider().Tracer(name, opts...)
})
_ = redisotel.NewTracingHook(redisotel.WithTracerProvider(tp))
_ = newTracingHook("", WithTracerProvider(tp))
if !invoked {
t.Fatalf("did not call custom TraceProvider")
@ -43,55 +36,52 @@ func TestNewWithTracerProvider(t *testing.T) {
func TestNewWithAttributes(t *testing.T) {
provider := sdktrace.NewTracerProvider()
hook := redisotel.NewTracingHook(redisotel.WithTracerProvider(provider), redisotel.WithAttributes(semconv.NetPeerNameKey.String("localhost")))
hook := newTracingHook("", WithTracerProvider(provider), WithAttributes(semconv.NetPeerNameKey.String("localhost")))
ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
cmd := redis.NewCmd(ctx, "ping")
defer span.End()
ctx, err := hook.BeforeProcess(ctx, cmd)
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
if !(attrs[0] == semconv.DBSystemRedis) {
t.Fatalf("expected attrs[0] to be semconv.DBSystemRedis, got: %v", attrs[0])
}
if !(attrs[1] == semconv.NetPeerNameKey.String("localhost")) {
t.Fatalf("expected attrs[1] to be semconv.NetPeerNameKey.String(\"localhost\"), got: %v", attrs[1])
}
if !(attrs[2] == semconv.DBStatementKey.String("ping")) {
t.Fatalf("expected attrs[2] to be semconv.DBStatementKey.String(\"ping\"), got: %v", attrs[2])
}
return nil
})
err := processHook(ctx, cmd)
if err != nil {
t.Fatal(err)
}
err = hook.AfterProcess(ctx, cmd)
if err != nil {
t.Fatal(err)
}
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
if !(attrs[0] == semconv.DBSystemRedis) {
t.Fatalf("expected attrs[0] to be semconv.DBSystemRedis, got: %v", attrs[0])
}
if !(attrs[1] == semconv.NetPeerNameKey.String("localhost")) {
t.Fatalf("expected attrs[1] to be semconv.NetPeerNameKey.String(\"localhost\"), got: %v", attrs[1])
}
if !(attrs[2] == semconv.DBStatementKey.String("ping")) {
t.Fatalf("expected attrs[2] to be semconv.DBStatementKey.String(\"ping\"), got: %v", attrs[2])
}
}
func TestWithDBStatement(t *testing.T) {
provider := sdktrace.NewTracerProvider()
hook := redisotel.NewTracingHook(
redisotel.WithTracerProvider(provider),
redisotel.WithDBStatement(false),
hook := newTracingHook(
"",
WithTracerProvider(provider),
WithDBStatement(false),
)
ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
cmd := redis.NewCmd(ctx, "ping")
defer span.End()
ctx, err := hook.BeforeProcess(ctx, cmd)
if err != nil {
t.Fatal(err)
}
err = hook.AfterProcess(ctx, cmd)
if err != nil {
t.Fatal(err)
}
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
for _, attr := range attrs {
if attr.Key == semconv.DBStatementKey {
t.Fatal("Attribute with db statement should not exist")
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
for _, attr := range attrs {
if attr.Key == semconv.DBStatementKey {
t.Fatal("Attribute with db statement should not exist")
}
}
return nil
})
err := processHook(ctx, cmd)
if err != nil {
t.Fatal(err)
}
}

175
extra/redisotel/tracing.go Normal file
View File

@ -0,0 +1,175 @@
package redisotel
import (
"context"
"fmt"
"net"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"go.opentelemetry.io/otel/trace"
"github.com/go-redis/redis/extra/rediscmd/v9"
"github.com/go-redis/redis/v9"
)
const (
instrumName = "github.com/go-redis/redis/extra/redisotel"
)
func InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error {
switch rdb := rdb.(type) {
case *redis.Client:
opt := rdb.Options()
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
return nil
case *redis.ClusterClient:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
return nil
case *redis.Ring:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
return nil
default:
return fmt.Errorf("redisotel: %T not supported", rdb)
}
}
type tracingHook struct {
conf *config
spanOpts []trace.SpanStartOption
}
var _ redis.Hook = (*tracingHook)(nil)
func newTracingHook(connString string, opts ...TracingOption) *tracingHook {
baseOpts := make([]baseOption, len(opts))
for i, opt := range opts {
baseOpts[i] = opt
}
conf := newConfig(baseOpts...)
if conf.tracer == nil {
conf.tracer = conf.tp.Tracer(
instrumName,
trace.WithInstrumentationVersion("semver:"+redis.Version()),
)
}
if connString != "" {
conf.attrs = append(conf.attrs, semconv.DBConnectionStringKey.String(connString))
}
return &tracingHook{
conf: conf,
spanOpts: []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(conf.attrs...),
},
}
}
func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, network, addr)
}
spanOpts := th.spanOpts
spanOpts = append(spanOpts, trace.WithAttributes(
attribute.String("network", network),
attribute.String("addr", addr),
))
ctx, span := th.conf.tracer.Start(ctx, "redis.dial", spanOpts...)
defer span.End()
conn, err := hook(ctx, network, addr)
if err != nil {
recordError(ctx, span, err)
return nil, err
}
return conn, nil
}
}
func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, cmd)
}
opts := th.spanOpts
if th.conf.dbStmtEnabled {
opts = append(opts, trace.WithAttributes(
semconv.DBStatementKey.String(rediscmd.CmdString(cmd))),
)
}
ctx, span := th.conf.tracer.Start(ctx, cmd.FullName(), opts...)
defer span.End()
if err := hook(ctx, cmd); err != nil {
recordError(ctx, span, err)
return err
}
return nil
}
}
func (th *tracingHook) ProcessPipelineHook(
hook redis.ProcessPipelineHook,
) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, cmds)
}
opts := th.spanOpts
opts = append(opts, trace.WithAttributes(
attribute.Int("db.redis.num_cmd", len(cmds)),
))
summary, cmdsString := rediscmd.CmdsString(cmds)
if th.conf.dbStmtEnabled {
opts = append(opts, trace.WithAttributes(semconv.DBStatementKey.String(cmdsString)))
}
ctx, span := th.conf.tracer.Start(ctx, "redis.pipeline "+summary, opts...)
defer span.End()
if err := hook(ctx, cmds); err != nil {
recordError(ctx, span, err)
return err
}
return nil
}
}
func recordError(ctx context.Context, span trace.Span, err error) {
if err != redis.Nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
}
func formatDBConnString(network, addr string) string {
if network == "tcp" {
network = "redis"
}
return fmt.Sprintf("%s://%s", network, addr)
}