diff --git a/contrib/go-redis/redis.v8/example_test.go b/contrib/go-redis/redis.v8/example_test.go new file mode 100644 index 0000000000..80e60b07a6 --- /dev/null +++ b/contrib/go-redis/redis.v8/example_test.go @@ -0,0 +1,58 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis_test + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis.v8" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// To start tracing Redis, simply create a new client using the library and continue +// using as you normally would. +func Example() { + ctx := context.Background() + // create a new Client + opts := &redis.Options{Addr: "127.0.0.1", Password: "", DB: 0} + c := redistrace.NewClient(opts) + + // any action emits a span + c.Set(ctx, "test_key", "test_value", 0) + + // optionally, create a new root span + root, ctx := tracer.StartSpanFromContext(context.Background(), "parent.request", + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName("web"), + tracer.ResourceName("/home"), + ) + + // commit further commands, which will inherit from the parent in the context. + c.Set(ctx, "food", "cheese", 0) + root.Finish() +} + +// You can also trace Redis Pipelines. Simply use as usual and the traces will be +// automatically picked up by the underlying implementation. +func Example_pipeliner() { + ctx := context.Background() + // create a client + opts := &redis.Options{Addr: "127.0.0.1", Password: "", DB: 0} + c := redistrace.NewClient(opts, redistrace.WithServiceName("my-redis-service")) + + // open the pipeline + pipe := c.Pipeline() + + // submit some commands + pipe.Incr(ctx, "pipeline_counter") + pipe.Expire(ctx, "pipeline_counter", time.Hour) + + // execute with trace + pipe.Exec(ctx) +} diff --git a/contrib/go-redis/redis.v8/option.go b/contrib/go-redis/redis.v8/option.go new file mode 100644 index 0000000000..dd8c660b19 --- /dev/null +++ b/contrib/go-redis/redis.v8/option.go @@ -0,0 +1,60 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis + +import ( + "math" + + "gopkg.in/DataDog/dd-trace-go.v1/internal" +) + +type clientConfig struct { + serviceName string + analyticsRate float64 +} + +// ClientOption represents an option that can be used to create or wrap a client. +type ClientOption func(*clientConfig) + +func defaults(cfg *clientConfig) { + cfg.serviceName = "redis.client" + // cfg.analyticsRate = globalconfig.AnalyticsRate() + if internal.BoolEnv("DD_TRACE_REDIS_ANALYTICS_ENABLED", false) { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } +} + +// WithServiceName sets the given service name for the client. +func WithServiceName(name string) ClientOption { + return func(cfg *clientConfig) { + cfg.serviceName = name + } +} + +// WithAnalytics enables Trace Analytics for all started spans. +func WithAnalytics(on bool) ClientOption { + return func(cfg *clientConfig) { + if on { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } + } +} + +// WithAnalyticsRate sets the sampling rate for Trace Analytics events +// correlated to started spans. +func WithAnalyticsRate(rate float64) ClientOption { + return func(cfg *clientConfig) { + if rate >= 0.0 && rate <= 1.0 { + cfg.analyticsRate = rate + } else { + cfg.analyticsRate = math.NaN() + } + } +} diff --git a/contrib/go-redis/redis.v8/redis.go b/contrib/go-redis/redis.v8/redis.go new file mode 100644 index 0000000000..18722fdde3 --- /dev/null +++ b/contrib/go-redis/redis.v8/redis.go @@ -0,0 +1,142 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +// Package redis provides tracing functions for tracing the go-redis/redis package (https://github.com/go-redis/redis). +// This package supports versions up to go-redis 6.15. +package redis + +import ( + "bytes" + "context" + + "math" + "net" + "strconv" + "strings" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "github.com/go-redis/redis/v8" +) + +type datadogHook struct { + *params +} + +// params holds the tracer and a set of parameters which are recorded with every trace. +type params struct { + host string + port string + db string + config *clientConfig +} + +// NewClient returns a new Client that is traced with the default tracer under +// the service name "redis". +func NewClient(opt *redis.Options, opts ...ClientOption) redis.UniversalClient { + cfg := new(clientConfig) + defaults(cfg) + for _, fn := range opts { + fn(cfg) + } + host, port, err := net.SplitHostPort(opt.Addr) + if err != nil { + host = opt.Addr + port = "6379" + } + params := ¶ms{ + host: host, + port: port, + db: strconv.Itoa(opt.DB), + config: cfg, + } + client := redis.NewClient(opt) + client.AddHook(&datadogHook{params: params}) + return client +} + +func (ddh *datadogHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + raw := cmd.String() + parts := strings.Split(raw, " ") + length := len(parts) - 1 + p := ddh.params + opts := []ddtrace.StartSpanOption{ + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName(p.config.serviceName), + tracer.ResourceName(parts[0]), + tracer.Tag(ext.TargetHost, p.host), + tracer.Tag(ext.TargetPort, p.port), + tracer.Tag("out.db", p.db), + tracer.Tag("redis.raw_command", raw), + tracer.Tag("redis.args_length", strconv.Itoa(length)), + } + if !math.IsNaN(p.config.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate)) + } + _, ctx = tracer.StartSpanFromContext(ctx, "redis.command", opts...) + return ctx, nil +} + +func (ddh *datadogHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + var span tracer.Span + span, _ = tracer.SpanFromContext(ctx) + var finishOpts []ddtrace.FinishOption + errRedis := cmd.Err() + if errRedis != redis.Nil { + finishOpts = append(finishOpts, tracer.WithError(errRedis)) + } + span.Finish(finishOpts...) + return nil +} + +func (ddh *datadogHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + raw := commandsToString(cmds) + parts := strings.Split(raw, " ") + length := len(parts) - 1 + p := ddh.params + opts := []ddtrace.StartSpanOption{ + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName(p.config.serviceName), + tracer.ResourceName(parts[0]), + tracer.Tag(ext.TargetHost, p.host), + tracer.Tag(ext.TargetPort, p.port), + tracer.Tag("out.db", p.db), + tracer.Tag("redis.raw_command", raw), + tracer.Tag("redis.args_length", strconv.Itoa(length)), + tracer.Tag(ext.ResourceName, raw), + tracer.Tag("redis.pipeline_length", strconv.Itoa(len(cmds))), + } + if !math.IsNaN(p.config.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate)) + } + _, ctx = tracer.StartSpanFromContext(ctx, "redis.command", opts...) + return ctx, nil +} + +func (ddh *datadogHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + var span tracer.Span + span, _ = tracer.SpanFromContext(ctx) + var finishOpts []ddtrace.FinishOption + for _, cmd := range cmds { + errCmd := cmd.Err() + if errCmd != redis.Nil { + finishOpts = append(finishOpts, tracer.WithError(errCmd)) + } + } + span.Finish(finishOpts...) + return nil +} + +// commandsToString returns a string representation of a slice of redis Commands, separated by newlines. +func commandsToString(cmds []redis.Cmder) string { + var b bytes.Buffer + for _, cmd := range cmds { + b.WriteString(cmd.String()) + b.WriteString("\n") + } + return b.String() +} diff --git a/contrib/go-redis/redis.v8/redis_test.go b/contrib/go-redis/redis.v8/redis_test.go new file mode 100644 index 0000000000..b40b306cb5 --- /dev/null +++ b/contrib/go-redis/redis.v8/redis_test.go @@ -0,0 +1,346 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +const debug = false + +// ensure it's a redis.Hook +var _ redis.Hook = (*datadogHook)(nil) + +func TestMain(m *testing.M) { + _, ok := os.LookupEnv("INTEGRATION") + if !ok { + fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable") + os.Exit(0) + } + os.Exit(m.Run()) +} + +func TestClientEvalSha(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + + sha1 := client.ScriptLoad(ctx, "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}").Val() + mt.Reset() + + client.EvalSha(ctx, sha1, []string{"key1", "key2", "first", "second"}) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("evalsha", span.Tag(ext.ResourceName)) +} + +func TestClient(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + client.Set(ctx, "test_key", "test_value", 0) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("set test_key test_value: ", span.Tag("redis.raw_command")) + assert.Equal("3", span.Tag("redis.args_length")) +} + +func TestPipeline(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + pipeline := client.Pipeline() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + + // Exec with context test + pipeline.Exec(ctx) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\n", span.Tag(ext.ResourceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("1", span.Tag("redis.pipeline_length")) + + mt.Reset() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + pipeline.Expire(ctx, "pipeline_counter_1", time.Minute) + + // Rewriting Exec + pipeline.Exec(ctx) + + spans = mt.FinishedSpans() + assert.Len(spans, 1) + + span = spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\nexpire pipeline_counter_1 60: false\n", span.Tag(ext.ResourceName)) + assert.Equal("2", span.Tag("redis.pipeline_length")) +} + +func TestChildSpan(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + // Parent span + client := NewClient(opts, WithServiceName("my-redis")) + root, ctx := tracer.StartSpanFromContext(ctx, "parent.span") + + client.Set(ctx, "test_key", "test_value", 0) + root.Finish() + + spans := mt.FinishedSpans() + assert.Len(spans, 2) + + var child, parent mocktracer.Span + for _, s := range spans { + // order of traces in buffer is not garanteed + switch s.OperationName() { + case "redis.command": + child = s + case "parent.span": + parent = s + } + } + assert.NotNil(parent) + assert.NotNil(child) + + assert.Equal(child.ParentID(), parent.SpanID()) + assert.Equal(child.Tag(ext.TargetHost), "127.0.0.1") + assert.Equal(child.Tag(ext.TargetPort), "6379") +} + +func TestMultipleCommands(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + client.Set(ctx, "test_key", "test_value", 0) + client.Get(ctx, "test_key") + client.Incr(ctx, "int_key") + client.ClientList(ctx) + + spans := mt.FinishedSpans() + assert.Len(spans, 4) + + // Checking all commands were recorded + var commands [4]string + for i := 0; i < 4; i++ { + commands[i] = spans[i].Tag("redis.raw_command").(string) + } + assert.Contains(commands, "set test_key test_value: ") + assert.Contains(commands, "get test_key: ") + assert.Contains(commands, "incr int_key: 0") + assert.Contains(commands, "client list: ") +} + +func TestError(t *testing.T) { + t.Run("wrong-port", func(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6378"} // wrong port + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + _, err := client.Get(ctx, "key").Result() + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + span := spans[0] + + assert.Equal("redis.command", span.OperationName()) + assert.NotNil(err) + assert.Equal(err, span.Tag(ext.Error)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6378", span.Tag(ext.TargetPort)) + assert.Equal("get key: ", span.Tag("redis.raw_command")) + }) + + t.Run("nil", func(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + _, err := client.Get(ctx, "non_existent_key").Result() + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + span := spans[0] + + assert.Equal(redis.Nil, err) + assert.Equal("redis.command", span.OperationName()) + assert.Empty(span.Tag(ext.Error)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("get non_existent_key: ", span.Tag("redis.raw_command")) + }) +} +func TestAnalyticsSettings(t *testing.T) { + assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...ClientOption) { + ctx := context.Background() + client := NewClient(&redis.Options{Addr: "127.0.0.1:6379"}, opts...) + client.Set(ctx, "test_key", "test_value", 0) + pipeline := client.Pipeline() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + pipeline.Exec(ctx) + + spans := mt.FinishedSpans() + assert.Len(t, spans, 2) + for _, s := range spans { + assert.Equal(t, rate, s.Tag(ext.EventSampleRate)) + } + } + + t.Run("defaults", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil) + }) + + t.Run("global", func(t *testing.T) { + t.Skip("global flag disabled") + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.4) + }) + + t.Run("enabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, 1.0, WithAnalytics(true)) + }) + + t.Run("disabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil, WithAnalytics(false)) + }) + + t.Run("override", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.23, WithAnalyticsRate(0.23)) + }) + + t.Run("zero", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, 0.0, WithAnalyticsRate(0.0)) + }) +} + +func TestWithContext(t *testing.T) { + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + ctx1 := context.Background() + ctx2 := context.Background() + client1 := NewClient(opts, WithServiceName("my-redis")) + s1, ctx1 := tracer.StartSpanFromContext(ctx1, "span1.name") + + s2, ctx2 := tracer.StartSpanFromContext(ctx2, "span2.name") + client2 := NewClient(opts, WithServiceName("my-redis")) + client1.Set(ctx1, "test_key", "test_value", 0) + client2.Get(ctx2, "test_key") + s1.Finish() + s2.Finish() + + spans := mt.FinishedSpans() + assert.Len(spans, 4) + var span1, span2, setSpan, getSpan mocktracer.Span + for _, s := range spans { + switch s.Tag(ext.ResourceName) { + case "span1.name": + span1 = s + case "span2.name": + span2 = s + case "set": + setSpan = s + case "get": + getSpan = s + } + } + + assert.NotNil(span1) + assert.NotNil(span2) + assert.NotNil(setSpan) + assert.NotNil(getSpan) + assert.Equal(span1.SpanID(), setSpan.ParentID()) + assert.Equal(span2.SpanID(), getSpan.ParentID()) +}