154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
|
// Package redigo provides functions to trace the garyburd/redigo package (https://github.com/garyburd/redigo).
|
||
|
package redigo
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/url"
|
||
|
"strconv"
|
||
|
|
||
|
"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"
|
||
|
|
||
|
redis "github.com/garyburd/redigo/redis"
|
||
|
)
|
||
|
|
||
|
// Conn is an implementation of the redis.Conn interface that supports tracing
|
||
|
type Conn struct {
|
||
|
redis.Conn
|
||
|
*params
|
||
|
}
|
||
|
|
||
|
// params contains fields and metadata useful for command tracing
|
||
|
type params struct {
|
||
|
config *dialConfig
|
||
|
network string
|
||
|
host string
|
||
|
port string
|
||
|
}
|
||
|
|
||
|
// parseOptions parses a set of arbitrary options (which can be of type redis.DialOption
|
||
|
// or the local DialOption) and returns the corresponding redis.DialOption set as well as
|
||
|
// a configured dialConfig.
|
||
|
func parseOptions(options ...interface{}) ([]redis.DialOption, *dialConfig) {
|
||
|
dialOpts := []redis.DialOption{}
|
||
|
cfg := new(dialConfig)
|
||
|
defaults(cfg)
|
||
|
for _, opt := range options {
|
||
|
switch o := opt.(type) {
|
||
|
case redis.DialOption:
|
||
|
dialOpts = append(dialOpts, o)
|
||
|
case DialOption:
|
||
|
o(cfg)
|
||
|
}
|
||
|
}
|
||
|
return dialOpts, cfg
|
||
|
}
|
||
|
|
||
|
// Dial dials into the network address and returns a traced redis.Conn.
|
||
|
// The set of supported options must be either of type redis.DialOption or this package's DialOption.
|
||
|
func Dial(network, address string, options ...interface{}) (redis.Conn, error) {
|
||
|
dialOpts, cfg := parseOptions(options...)
|
||
|
c, err := redis.Dial(network, address, dialOpts...)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
host, port, err := net.SplitHostPort(address)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
tc := Conn{c, ¶ms{cfg, network, host, port}}
|
||
|
return tc, nil
|
||
|
}
|
||
|
|
||
|
// DialURL connects to a Redis server at the given URL using the Redis
|
||
|
// URI scheme. URLs should follow the draft IANA specification for the
|
||
|
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
|
||
|
// The returned redis.Conn is traced.
|
||
|
func DialURL(rawurl string, options ...interface{}) (redis.Conn, error) {
|
||
|
dialOpts, cfg := parseOptions(options...)
|
||
|
u, err := url.Parse(rawurl)
|
||
|
if err != nil {
|
||
|
return Conn{}, err
|
||
|
}
|
||
|
host, port, err := net.SplitHostPort(u.Host)
|
||
|
if err != nil {
|
||
|
host = u.Host
|
||
|
port = "6379"
|
||
|
}
|
||
|
if host == "" {
|
||
|
host = "localhost"
|
||
|
}
|
||
|
network := "tcp"
|
||
|
c, err := redis.DialURL(rawurl, dialOpts...)
|
||
|
tc := Conn{c, ¶ms{cfg, network, host, port}}
|
||
|
return tc, err
|
||
|
}
|
||
|
|
||
|
// newChildSpan creates a span inheriting from the given context. It adds to the span useful metadata about the traced Redis connection
|
||
|
func (tc Conn) newChildSpan(ctx context.Context) ddtrace.Span {
|
||
|
p := tc.params
|
||
|
span, _ := tracer.StartSpanFromContext(ctx, "redis.command",
|
||
|
tracer.SpanType(ext.SpanTypeRedis),
|
||
|
tracer.ServiceName(p.config.serviceName),
|
||
|
)
|
||
|
span.SetTag("out.network", p.network)
|
||
|
span.SetTag(ext.TargetPort, p.port)
|
||
|
span.SetTag(ext.TargetHost, p.host)
|
||
|
return span
|
||
|
}
|
||
|
|
||
|
// Do wraps redis.Conn.Do. It sends a command to the Redis server and returns the received reply.
|
||
|
// In the process it emits a span containing key information about the command sent.
|
||
|
// When passed a context.Context as the final argument, Do will ensure that any span created
|
||
|
// inherits from this context. The rest of the arguments are passed through to the Redis server unchanged.
|
||
|
func (tc Conn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||
|
var (
|
||
|
ctx context.Context
|
||
|
ok bool
|
||
|
)
|
||
|
if n := len(args); n > 0 {
|
||
|
ctx, ok = args[n-1].(context.Context)
|
||
|
if ok {
|
||
|
args = args[:n-1]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
span := tc.newChildSpan(ctx)
|
||
|
defer func() {
|
||
|
span.Finish(tracer.WithError(err))
|
||
|
}()
|
||
|
|
||
|
span.SetTag("redis.args_length", strconv.Itoa(len(args)))
|
||
|
|
||
|
if len(commandName) > 0 {
|
||
|
span.SetTag(ext.ResourceName, commandName)
|
||
|
} else {
|
||
|
// When the command argument to the Do method is "", then the Do method will flush the output buffer
|
||
|
// See https://godoc.org/github.com/garyburd/redigo/redis#hdr-Pipelining
|
||
|
span.SetTag(ext.ResourceName, "redigo.Conn.Flush")
|
||
|
}
|
||
|
var b bytes.Buffer
|
||
|
b.WriteString(commandName)
|
||
|
for _, arg := range args {
|
||
|
b.WriteString(" ")
|
||
|
switch arg := arg.(type) {
|
||
|
case string:
|
||
|
b.WriteString(arg)
|
||
|
case int:
|
||
|
b.WriteString(strconv.Itoa(arg))
|
||
|
case int32:
|
||
|
b.WriteString(strconv.FormatInt(int64(arg), 10))
|
||
|
case int64:
|
||
|
b.WriteString(strconv.FormatInt(arg, 10))
|
||
|
case fmt.Stringer:
|
||
|
b.WriteString(arg.String())
|
||
|
}
|
||
|
}
|
||
|
span.SetTag("redis.raw_command", b.String())
|
||
|
return tc.Conn.Do(commandName, args...)
|
||
|
}
|