Skip to content

Commit

Permalink
Raphael/go redis (#47)
Browse files Browse the repository at this point in the history
* Working go-redis integration with 1st test in test-suit

* adding parent-child span test, cleaning code

* test multiple commands

* test errors propagation

* todo check multiple test names (separate go routines asynchronous pbms

* Casting tests to write types

* After Aaditya's review

* tracing pipelines with test

* Overiding Pipeline()

* go-redis: ensure parent context propagation

* go-redis: fix tests for context

* After Aaditya's review

* Rewriting Exec() method and having a separate ExecWithContext()

* Correction for tests

* Adding TraceParams + commets

* After Christian's review

* docker-compose agent correction

* go-redis: examples

* go-redis: examples

* go-redis: touch up examples

* go-redis doc fix

* goredis doc fix

* go-redis pipeline -> pipeliner

* np: example
  • Loading branch information
raphaelgavache committed May 9, 2017
1 parent fbe92b2 commit c2f3125
Show file tree
Hide file tree
Showing 3 changed files with 451 additions and 0 deletions.
81 changes: 81 additions & 0 deletions tracer/contrib/go-redis/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package goredistrace_test

import (
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/gin-gonic/gintrace"
"github.com/DataDog/dd-trace-go/tracer/contrib/go-redis"
"github.com/gin-gonic/gin"
redis "github.com/go-redis/redis"
"time"
)

// To start tracing Redis commands, use the NewTracedClient function to create a traced Redis clienty,
// passing in a service name of choice.
func Example() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := goredistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// Emit spans per command by using your Redis connection as usual
c.Set("test_key", "test_value", 0)

// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())

// When set with a context, the traced client will emit a span inheriting from 'parent.request'
c.SetContext(ctx)
c.Set("food", "cheese", 0)
root.Finish()

// Contexts can be easily passed between Datadog integrations
r := gin.Default()
r.Use(gintrace.Middleware("web-admin"))
client := goredistrace.NewTracedClient(opts, tracer.DefaultTracer, "redis-img-backend")

r.GET("/user/settings/:id", func(ctx *gin.Context) {
// create a span that is a child of your http request
client.SetContext(ctx)
client.Get(fmt.Sprintf("cached_user_details_%s", ctx.Param("id")))
})
}

// You can also trace Redis Pipelines
func Example_pipeline() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := goredistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// p is a TracedPipeliner
pipe := c.Pipeline()
pipe.Incr("pipeline_counter")
pipe.Expire("pipeline_counter", time.Hour)

pipe.Exec()
}

func ExampleNewTracedClient() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := goredistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// Emit spans per command by using your Redis connection as usual
c.Set("test_key", "test_value", 0)

// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())

// When set with a context, the traced client will emit a span inheriting from 'parent.request'
c.SetContext(ctx)
c.Set("food", "cheese", 0)
root.Finish()
}
151 changes: 151 additions & 0 deletions tracer/contrib/go-redis/tracedredis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Package goredistrace provides tracing for the go-redis Redis client (https://github.com/go-redis/redis)
package goredistrace

import (
"bytes"
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/go-redis/redis"
"strconv"
"strings"
)

// TracedClient is used to trace requests to a redis server.
type TracedClient struct {
*redis.Client
traceParams traceParams
}

// TracedPipeline is used to trace pipelines executed on a redis server.
type TracedPipeliner struct {
redis.Pipeliner
traceParams traceParams
}

type traceParams struct {
host string
port string
db string
service string
tracer *tracer.Tracer
}

// NewTracedClient takes a Client returned by redis.NewClient and configures it to emit spans under the given service name
func NewTracedClient(opt *redis.Options, t *tracer.Tracer, service string) *TracedClient {
var host, port string
addr := strings.Split(opt.Addr, ":")
if len(addr) == 2 && addr[1] != "" {
port = addr[1]
} else {
port = "6379"
}
host = addr[0]
db := strconv.Itoa(opt.DB)

client := redis.NewClient(opt)
t.SetServiceInfo(service, "redis", ext.AppTypeDB)
tc := &TracedClient{
client,
traceParams{
host,
port,
db,
service,
t},
}

tc.Client.WrapProcess(createWrapperFromClient(tc))
return tc
}

// Pipeline creates a TracedPipeline from a TracedClient
func (c *TracedClient) Pipeline() *TracedPipeliner {
return &TracedPipeliner{
c.Client.Pipeline(),
c.traceParams,
}
}

// ExecWithContext calls Pipeline.Exec(). It ensures that the resulting Redis calls
// are traced, and that emitted spans are children of the given Context
func (c *TracedPipeliner) ExecWithContext(ctx context.Context) ([]redis.Cmder, error) {
span := c.traceParams.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = c.traceParams.service

span.SetMeta("out.host", c.traceParams.host)
span.SetMeta("out.port", c.traceParams.port)
span.SetMeta("out.db", c.traceParams.db)

cmds, err := c.Pipeliner.Exec()
if err != nil {
span.SetError(err)
}
span.Resource = String(cmds)
span.SetMeta("redis.pipeline_length", strconv.Itoa(len(cmds)))
span.Finish()
return cmds, err
}

// Exec calls Pipeline.Exec() ensuring that the resulting Redis calls are traced
func (c *TracedPipeliner) Exec() ([]redis.Cmder, error) {
span := c.traceParams.tracer.NewRootSpan("redis.command", c.traceParams.service, "redis")

span.SetMeta("out.host", c.traceParams.host)
span.SetMeta("out.port", c.traceParams.port)
span.SetMeta("out.db", c.traceParams.db)

cmds, err := c.Pipeliner.Exec()
if err != nil {
span.SetError(err)
}
span.Resource = String(cmds)
span.SetMeta("redis.pipeline_length", strconv.Itoa(len(cmds)))
span.Finish()
return cmds, err
}

// String returns a string representation of a slice of redis Commands, separated by newlines
func String(cmds []redis.Cmder) string {
var b bytes.Buffer
for _, cmd := range cmds {
b.WriteString(cmd.String())
b.WriteString("\n")
}
return b.String()
}

// SetContext sets a context on a TracedClient. Use it to ensure that emitted spans have the correct parent
func (c *TracedClient) SetContext(ctx context.Context) {
c.Client = c.Client.WithContext(ctx)
}

// createWrapperFromClient wraps tracing into redis.Process().
func createWrapperFromClient(tc *TracedClient) func(oldProcess func(cmd redis.Cmder) error) func(cmd redis.Cmder) error {
return func(oldProcess func(cmd redis.Cmder) error) func(cmd redis.Cmder) error {
return func(cmd redis.Cmder) error {
ctx := tc.Client.Context()

var resource string
resource = strings.Split(cmd.String(), " ")[0]
args_length := len(strings.Split(cmd.String(), " ")) - 1
span := tc.traceParams.tracer.NewChildSpanFromContext("redis.command", ctx)

span.Service = tc.traceParams.service
span.Resource = resource

span.SetMeta("redis.raw_command", cmd.String())
span.SetMeta("redis.args_length", strconv.Itoa(args_length))
span.SetMeta("out.host", tc.traceParams.host)
span.SetMeta("out.port", tc.traceParams.port)
span.SetMeta("out.db", tc.traceParams.db)

err := oldProcess(cmd)
if err != nil {
span.SetError(err)
}
span.Finish()
return err
}
}
}
Loading

0 comments on commit c2f3125

Please sign in to comment.