Skip to content

Commit

Permalink
support go-redis.v8
Browse files Browse the repository at this point in the history
Signed-off-by: sacha-froment-ext <sfroment42@gmail.com>
  • Loading branch information
sfroment committed Sep 11, 2020
1 parent 498a455 commit cb69061
Show file tree
Hide file tree
Showing 4 changed files with 624 additions and 0 deletions.
58 changes: 58 additions & 0 deletions contrib/go-redis/redis.v8/example_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
60 changes: 60 additions & 0 deletions contrib/go-redis/redis.v8/option.go
Original file line number Diff line number Diff line change
@@ -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 "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/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()
}
}
}
163 changes: 163 additions & 0 deletions contrib/go-redis/redis.v8/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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"
"fmt"
"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 := &params{
host: host,
port: port,
db: strconv.Itoa(opt.DB),
config: cfg,
}
client := redis.NewClient(opt)
hook := &dataDogHook{params: params}
client.AddHook(hook)
return client
}

func (ddh *dataDogHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
raw := cmderToString(cmd)
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(cmderToString(cmd))
b.WriteString("\n")
}
return b.String()
}

func cmderToString(cmd redis.Cmder) string {
// We want to support multiple versions of the go-redis library. In
// older versions Cmder implements the Stringer interface, while in
// newer versions that was removed, and this String method which
// sometimes returns an error is used instead. By doing a type assertion
// we can support both versions.
if stringer, ok := cmd.(fmt.Stringer); ok {
return stringer.String()
}

args := cmd.Args()
if len(args) == 0 {
return ""
}
if str, ok := args[0].(string); ok {
return str
}
return ""
}
Loading

0 comments on commit cb69061

Please sign in to comment.