plugin/etcdv3: Add etcd v3 plugin ()

* Update dependencies and add etcdv3 client

* Update etcd plugin to support etcd v3 clients

Fixes 
This commit is contained in:
Nitish Tiwari 2018-06-30 20:49:13 +05:30 committed by Miek Gieben
parent f3afd70021
commit 6fe27d99be
10327 changed files with 4196998 additions and 82 deletions
.travis.ymlGopkg.lockGopkg.toml
plugin/etcd
test
vendor/github.com/DataDog/dd-trace-go

View file

@ -15,10 +15,10 @@ git:
depth: 3 depth: 3
env: env:
- TEST_TYPE=coverage ETCD_VERSION=2.3.1 - TEST_TYPE=coverage ETCD_VERSION=3.3.8
- TEST_TYPE=integration ETCD_VERSION=2.3.1 - TEST_TYPE=integration ETCD_VERSION=3.3.8
- TEST_TYPE=core ETCD_VERSION=2.3.1 - TEST_TYPE=core ETCD_VERSION=3.3.8
- TEST_TYPE=plugin ETCD_VERSION=2.3.1 - TEST_TYPE=plugin ETCD_VERSION=3.3.8
# In the Travis VM-based build environment, IPv6 networking is not # In the Travis VM-based build environment, IPv6 networking is not
# enabled by default. The sysctl operations below enable IPv6. # enabled by default. The sysctl operations below enable IPv6.

10
Gopkg.lock generated
View file

@ -60,7 +60,12 @@
[[projects]] [[projects]]
name = "github.com/coreos/etcd" name = "github.com/coreos/etcd"
packages = [ packages = [
"auth/authpb",
"client", "client",
"clientv3",
"etcdserver/api/v3rpc/rpctypes",
"etcdserver/etcdserverpb",
"mvcc/mvccpb",
"pkg/pathutil", "pkg/pathutil",
"pkg/srv", "pkg/srv",
"pkg/types", "pkg/types",
@ -132,7 +137,9 @@
[[projects]] [[projects]]
name = "github.com/gogo/protobuf" name = "github.com/gogo/protobuf"
packages = [ packages = [
"gogoproto",
"proto", "proto",
"protoc-gen-gogo/descriptor",
"sortkeys" "sortkeys"
] ]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c" revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
@ -391,6 +398,7 @@
"encoding/proto", "encoding/proto",
"grpclb/grpc_lb_v1/messages", "grpclb/grpc_lb_v1/messages",
"grpclog", "grpclog",
"health/grpc_health_v1",
"internal", "internal",
"keepalive", "keepalive",
"metadata", "metadata",
@ -560,6 +568,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "dbbdcbcd4c0e11f040230e43a145f113ed7e67ff2c52b2a5830e117c16a23630" inputs-digest = "435926fcc83a4f1a93fd257248f2b1256eaa5a212159b07743408b8cafdbffff"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View file

@ -26,3 +26,7 @@ ignored = [
[[override]] [[override]]
name = "github.com/ugorji/go" name = "github.com/ugorji/go"
revision = "f3cacc17c85ecb7f1b6a9e373ee85d1480919868" revision = "f3cacc17c85ecb7f1b6a9e373ee85d1480919868"
[[constraint]]
name = "github.com/coreos/etcd"
version = "3.3.5"

View file

@ -2,11 +2,11 @@
## Name ## Name
*etcd* - enables reading zone data from an etcd instance. *etcd* - enables reading zone data from an etcd version 3 instance.
## Description ## Description
The data in etcd has to be encoded as The data in etcd instance has to be encoded as
a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26) a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26)
like [SkyDNS](https://github.com/skynetservices/skydns). It should also work just like SkyDNS. like [SkyDNS](https://github.com/skynetservices/skydns). It should also work just like SkyDNS.
@ -21,7 +21,7 @@ etcd [ZONES...]
* **ZONES** zones etcd should be authoritative for. * **ZONES** zones etcd should be authoritative for.
The path will default to `/skydns` the local etcd proxy (http://localhost:2379). If no zones are The path will default to `/skydns` the local etcd3 proxy (http://localhost:2379). If no zones are
specified the block's zone will be used as the zone. specified the block's zone will be used as the zone.
If you want to `round robin` A and AAAA responses look at the `loadbalance` plugin. If you want to `round robin` A and AAAA responses look at the `loadbalance` plugin.
@ -169,7 +169,3 @@ dig +short skydns.local AAAA @localhost
2003::8:1 2003::8:1
2003::8:2 2003::8:2
~~~ ~~~
## Bugs
Only the etcdv2 protocol is supported.

View file

@ -1,9 +1,10 @@
// Package etcd provides the etcd backend plugin. // Package etcd provides the etcd version 3 backend plugin.
package etcd package etcd
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -15,10 +16,19 @@ import (
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
"github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/pkg/upstream"
etcdc "github.com/coreos/etcd/client" etcdcv3 "github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
const (
priority = 10 // default priority when nothing is set
ttl = 300 // default ttl when nothing is set
etcdTimeout = 5 * time.Second
)
var errKeyNotFound = errors.New("Key not found")
// Etcd is a plugin talks to an etcd cluster. // Etcd is a plugin talks to an etcd cluster.
type Etcd struct { type Etcd struct {
Next plugin.Handler Next plugin.Handler
@ -26,7 +36,7 @@ type Etcd struct {
Zones []string Zones []string
PathPrefix string PathPrefix string
Upstream upstream.Upstream // Proxy for looking up names during the resolution process Upstream upstream.Upstream // Proxy for looking up names during the resolution process
Client etcdc.KeysAPI Client *etcdcv3.Client
Ctx context.Context Ctx context.Context
Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving. Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving.
@ -56,10 +66,7 @@ func (e *Etcd) Lookup(state request.Request, name string, typ uint16) (*dns.Msg,
// IsNameError implements the ServiceBackend interface. // IsNameError implements the ServiceBackend interface.
func (e *Etcd) IsNameError(err error) bool { func (e *Etcd) IsNameError(err error) bool {
if ee, ok := err.(etcdc.Error); ok && ee.Code == etcdc.ErrorCodeKeyNotFound { return err == errKeyNotFound
return true
}
return false
} }
// Records looks up records in etcd. If exact is true, it will lookup just this // Records looks up records in etcd. If exact is true, it will lookup just this
@ -73,51 +80,50 @@ func (e *Etcd) Records(state request.Request, exact bool) ([]msg.Service, error)
return nil, err return nil, err
} }
segments := strings.Split(msg.Path(name, e.PathPrefix), "/") segments := strings.Split(msg.Path(name, e.PathPrefix), "/")
switch { return e.loopNodes(r.Kvs, segments, star)
case exact && r.Node.Dir:
return nil, nil
case r.Node.Dir:
return e.loopNodes(r.Node.Nodes, segments, star, nil)
default:
return e.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil)
}
} }
// get is a wrapper for client.Get func (e *Etcd) get(path string, recursive bool) (*etcdcv3.GetResponse, error) {
func (e *Etcd) get(path string, recursive bool) (*etcdc.Response, error) {
ctx, cancel := context.WithTimeout(e.Ctx, etcdTimeout) ctx, cancel := context.WithTimeout(e.Ctx, etcdTimeout)
defer cancel() defer cancel()
r, err := e.Client.Get(ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive}) if recursive == true {
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
r, err := e.Client.Get(ctx, path, etcdcv3.WithPrefix())
if err != nil {
return nil, err
}
if r.Count == 0 {
path = strings.TrimSuffix(path, "/")
r, err = e.Client.Get(ctx, path)
if err != nil {
return nil, err
}
if r.Count == 0 {
return nil, errKeyNotFound
}
}
return r, nil
}
r, err := e.Client.Get(ctx, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if r.Count == 0 {
return nil, errKeyNotFound
}
return r, nil return r, nil
} }
// skydns/local/skydns/east/staging/web func (e *Etcd) loopNodes(kv []*mvccpb.KeyValue, nameParts []string, star bool) (sx []msg.Service, err error) {
// skydns/local/skydns/west/production/web bx := make(map[msg.Service]bool)
//
// skydns/local/skydns/*/*/web
// skydns/local/skydns/*/web
// loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname
// will be match against any wildcards when star is true.
func (e *Etcd) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) {
if bx == nil {
bx = make(map[msg.Service]bool)
}
Nodes: Nodes:
for _, n := range ns { for _, n := range kv {
if n.Dir {
nodes, err := e.loopNodes(n.Nodes, nameParts, star, bx)
if err != nil {
return nil, err
}
sx = append(sx, nodes...)
continue
}
if star { if star {
keyParts := strings.Split(n.Key, "/") s := string(n.Key)
keyParts := strings.Split(s, "/")
for i, n := range nameParts { for i, n := range nameParts {
if i > len(keyParts)-1 { if i > len(keyParts)-1 {
// name is longer than key // name is longer than key
@ -132,16 +138,16 @@ Nodes:
} }
} }
serv := new(msg.Service) serv := new(msg.Service)
if err := json.Unmarshal([]byte(n.Value), serv); err != nil { if err := json.Unmarshal(n.Value, serv); err != nil {
return nil, fmt.Errorf("%s: %s", n.Key, err.Error()) return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
} }
b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text, Key: n.Key} b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text, Key: string(n.Key)}
if _, ok := bx[b]; ok { if _, ok := bx[b]; ok {
continue continue
} }
bx[b] = true bx[b] = true
serv.Key = n.Key serv.Key = string(n.Key)
serv.TTL = e.TTL(n, serv) serv.TTL = e.TTL(n, serv)
if serv.Priority == 0 { if serv.Priority == 0 {
serv.Priority = priority serv.Priority = priority
@ -153,8 +159,8 @@ Nodes:
// TTL returns the smaller of the etcd TTL and the service's // TTL returns the smaller of the etcd TTL and the service's
// TTL. If neither of these are set (have a zero value), a default is used. // TTL. If neither of these are set (have a zero value), a default is used.
func (e *Etcd) TTL(node *etcdc.Node, serv *msg.Service) uint32 { func (e *Etcd) TTL(kv *mvccpb.KeyValue, serv *msg.Service) uint32 {
etcdTTL := uint32(node.TTL) etcdTTL := uint32(kv.Lease)
if etcdTTL == 0 && serv.TTL == 0 { if etcdTTL == 0 && serv.TTL == 0 {
return ttl return ttl
@ -170,9 +176,3 @@ func (e *Etcd) TTL(node *etcdc.Node, serv *msg.Service) uint32 {
} }
return serv.TTL return serv.TTL
} }
const (
priority = 10 // default priority when nothing is set
ttl = 300 // default ttl when nothing is set
etcdTimeout = 5 * time.Second
)

View file

@ -65,8 +65,7 @@ func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
_, err = plugin.A(e, zone, state, nil, opt) _, err = plugin.A(e, zone, state, nil, opt)
} }
if err != nil && e.IsNameError(err) {
if e.IsNameError(err) {
if e.Fall.Through(state.Name()) { if e.Fall.Through(state.Name()) {
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)
} }

View file

@ -15,7 +15,6 @@ import (
"github.com/coredns/coredns/plugin/proxy" "github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
etcdc "github.com/coreos/etcd/client"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -300,12 +299,12 @@ func set(t *testing.T, e *Etcd, k string, ttl time.Duration, m *msg.Service) {
t.Fatal(err) t.Fatal(err)
} }
path, _ := msg.PathWithWildcard(k, e.PathPrefix) path, _ := msg.PathWithWildcard(k, e.PathPrefix)
e.Client.Set(ctxt, path, string(b), &etcdc.SetOptions{TTL: ttl}) e.Client.KV.Put(ctxt, path, string(b))
} }
func delete(t *testing.T, e *Etcd, k string) { func delete(t *testing.T, e *Etcd, k string) {
path, _ := msg.PathWithWildcard(k, e.PathPrefix) path, _ := msg.PathWithWildcard(k, e.PathPrefix)
e.Client.Delete(ctxt, path, &etcdc.DeleteOptions{Recursive: false}) e.Client.Delete(ctxt, path)
} }
func TestLookup(t *testing.T) { func TestLookup(t *testing.T) {

View file

@ -11,7 +11,7 @@ import (
"github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/plugin/proxy" "github.com/coredns/coredns/plugin/proxy"
etcdc "github.com/coreos/etcd/client" etcdcv3 "github.com/coreos/etcd/clientv3"
"github.com/mholt/caddy" "github.com/mholt/caddy"
) )
@ -124,22 +124,22 @@ func etcdParse(c *caddy.Controller) (*Etcd, bool, error) {
} }
etc.Client = client etc.Client = client
etc.endpoints = endpoints etc.endpoints = endpoints
return &etc, stubzones, nil return &etc, stubzones, nil
} }
return &Etcd{}, false, nil return &Etcd{}, false, nil
} }
func newEtcdClient(endpoints []string, cc *tls.Config) (etcdc.KeysAPI, error) { func newEtcdClient(endpoints []string, cc *tls.Config) (*etcdcv3.Client, error) {
etcdCfg := etcdc.Config{ etcdCfg := etcdcv3.Config{
Endpoints: endpoints, Endpoints: endpoints,
Transport: mwtls.NewHTTPSTransport(cc), TLS: cc,
} }
cli, err := etcdc.New(etcdCfg) cli, err := etcdcv3.New(etcdCfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return etcdc.NewKeysAPI(cli), nil return cli, nil
} }
const defaultEndpoint = "http://localhost:2379" const defaultEndpoint = "http://localhost:2379"

View file

@ -16,17 +16,16 @@ import (
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
etcdc "github.com/coreos/etcd/client" etcdcv3 "github.com/coreos/etcd/clientv3"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func etcdPlugin() *etcd.Etcd { func etcdPlugin() *etcd.Etcd {
etcdCfg := etcdc.Config{ etcdCfg := etcdcv3.Config{
Endpoints: []string{"http://localhost:2379"}, Endpoints: []string{"http://localhost:2379"},
} }
cli, _ := etcdc.New(etcdCfg) cli, _ := etcdcv3.New(etcdCfg)
client := etcdc.NewKeysAPI(cli) return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"}
return &etcd.Etcd{Client: client, PathPrefix: "/skydns"}
} }
// This test starts two coredns servers (and needs etcd). Configure a stubzones in both (that will loop) and // This test starts two coredns servers (and needs etcd). Configure a stubzones in both (that will loop) and
@ -94,11 +93,11 @@ func set(ctx context.Context, t *testing.T, e *etcd.Etcd, k string, ttl time.Dur
t.Fatal(err) t.Fatal(err)
} }
path, _ := msg.PathWithWildcard(k, e.PathPrefix) path, _ := msg.PathWithWildcard(k, e.PathPrefix)
e.Client.Set(ctx, path, string(b), &etcdc.SetOptions{TTL: ttl}) e.Client.KV.Put(ctx, path, string(b))
} }
// Copied from plugin/etcd/setup_test.go // Copied from plugin/etcd/setup_test.go
func delete(ctx context.Context, t *testing.T, e *etcd.Etcd, k string) { func delete(ctx context.Context, t *testing.T, e *etcd.Etcd, k string) {
path, _ := msg.PathWithWildcard(k, e.PathPrefix) path, _ := msg.PathWithWildcard(k, e.PathPrefix)
e.Client.Delete(ctx, path, &etcdc.DeleteOptions{Recursive: false}) e.Client.Delete(ctx, path)
} }

View file

@ -0,0 +1,36 @@
# Libraries supported for tracing
All of these libraries are supported by our Application Performance Monitoring tool.
## Usage
1. Check if your library is supported (*i.e.* you find it in this directory).
*ex:* if you're using the `net/http` package for your server, you see it's present in this directory.
2. In your app, replace your import by our traced version of the library.
*ex:*
```go
import "net/http"
```
becomes
```go
import "github.com/DataDog/dd-trace-go/contrib/net/http"
```
3. Read through the `example_test.go` present in each folder of the libraries to understand how to trace your app.
*ex:* for `net/http`, see [net/http/example_test.go](https://github.com/DataDog/dd-trace-go/blob/master/contrib/net/http/example_test.go)
## Contribution guidelines
### 1. Follow the package naming convention
If a library looks like this: `github.com/user/lib`, the contribution must looks like this `user/lib`.
In the case of the standard library, just use the path after `src`.
*E.g.* `src/database/sql` becomes `database/sql`.
### 2. Respect the original API
Keep the original names for exported functions, don't use the prefix or suffix `trace`.
*E.g.* prefer `Open` instead of `OpenTrace`.
Of course you can modify the number of arguments of a function if you need to pass the tracer for example.

View file

@ -0,0 +1,169 @@
package sql_test
import (
"context"
"log"
sqltrace "github.com/DataDog/dd-trace-go/contrib/database/sql"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
)
// To trace the sql calls, you just need to open your sql.DB with OpenTraced.
// All calls through this sql.DB object will then be traced.
func Example() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltrace.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// All calls through the database/sql API will then be traced.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// If you want to link your db calls with existing traces, you need to use
// the context version of the database/sql API.
// Just make sure you are passing the parent span within the context.
func Example_context() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltrace.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// We create a parent span and put it within the context.
span := tracer.NewRootSpan("postgres.parent", "web-backend", "query-parent")
ctx := tracer.ContextWithSpan(context.Background(), span)
// We need to use the context version of the database/sql API
// in order to link this call with the parent span.
db.PingContext(ctx)
rows, _ := db.QueryContext(ctx, "SELECT * FROM city LIMIT 5")
rows.Close()
stmt, _ := db.PrepareContext(ctx, "INSERT INTO city(name) VALUES($1)")
stmt.Exec("New York")
stmt, _ = db.PrepareContext(ctx, "SELECT name FROM city LIMIT $1")
rows, _ = stmt.Query(1)
rows.Close()
stmt.Close()
tx, _ := db.BeginTx(ctx, nil)
tx.ExecContext(ctx, "INSERT INTO city(name) VALUES('New York')")
rows, _ = tx.QueryContext(ctx, "SELECT * FROM city LIMIT 5")
rows.Close()
stmt, _ = tx.PrepareContext(ctx, "SELECT name FROM city LIMIT $1")
rows, _ = stmt.Query(1)
rows.Close()
stmt.Close()
tx.Commit()
// Calling span.Finish() will send the span into the tracer's buffer
// and then being processed.
span.Finish()
}
// You can trace all drivers implementing the database/sql/driver interface.
// For example, you can trace the go-sql-driver/mysql with the following code.
func Example_mySQL() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltrace.OpenTraced(&mysql.MySQLDriver{}, "user:password@/dbname", "web-backend")
if err != nil {
log.Fatal(err)
}
// All calls through the database/sql API will then be traced.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
func ExampleOpenTraced() {
// The first argument is a reference to the driver to trace.
// The second argument is the dataSourceName.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
// The last argument allows you to specify a custom tracer to use for tracing.
db, err := sqltrace.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// You can use a custom tracer by passing it through the optional last argument of OpenTraced.
func ExampleOpenTraced_tracer() {
// Create and customize a new tracer that will forward 50% of generated traces to the agent.
// (useful to manage resource usage in high-throughput environments)
trc := tracer.NewTracer()
trc.SetSampleRate(0.5)
// Pass your custom tracer through the last argument of OpenTraced to trace your db calls with it.
db, err := sqltrace.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend", trc)
if err != nil {
log.Fatal(err)
}
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// If you need more granularity, you can register the traced driver seperately from the Open call.
func ExampleRegister() {
// Register a traced version of your driver.
sqltrace.Register("postgres", &pq.Driver{})
// Returns a sql.DB object that holds the traced connection to the database.
// Note: the sql.DB object returned by sql.Open will not be traced so make sure to use sql.Open.
db, _ := sqltrace.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
defer db.Close()
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// You can use a custom tracer by passing it through the optional last argument of Register.
func ExampleRegister_tracer() {
// Create and customize a new tracer that will forward 50% of generated traces to the agent.
// (useful to manage resource usage in high-throughput environments)
trc := tracer.NewTracer()
trc.SetSampleRate(0.5)
// Register a traced version of your driver and specify to use the previous tracer
// to send the traces to the agent.
sqltrace.Register("postgres", &pq.Driver{}, trc)
// Returns a sql.DB object that holds the traced connection to the database.
// Note: the sql.DB object returned by sql.Open will not be traced so make sure to use sql.Open.
db, _ := sqltrace.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
defer db.Close()
}

View file

@ -0,0 +1,41 @@
package sql
import (
"log"
"testing"
"github.com/DataDog/dd-trace-go/contrib/database/sql/sqltest"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/go-sql-driver/mysql"
)
func TestMySQL(t *testing.T) {
trc, transport := tracertest.GetTestTracer()
db, err := OpenTraced(&mysql.MySQLDriver{}, "test:test@tcp(127.0.0.1:53306)/test", "mysql-test", trc)
if err != nil {
log.Fatal(err)
}
defer db.Close()
testDB := &sqltest.DB{
DB: db,
Tracer: trc,
Transport: transport,
DriverName: "mysql",
}
expectedSpan := &tracer.Span{
Name: "mysql.query",
Service: "mysql-test",
Type: "sql",
}
expectedSpan.Meta = map[string]string{
"db.user": "test",
"out.host": "127.0.0.1",
"out.port": "53306",
"db.name": "test",
}
sqltest.AllSQLTests(t, testDB, expectedSpan)
}

View file

@ -0,0 +1,42 @@
package sql
import (
"github.com/DataDog/dd-trace-go/contrib/database/sql/parsedsn"
)
// parseDSN returns all information passed through the DSN:
func parseDSN(driverName, dsn string) (meta map[string]string, err error) {
switch driverName {
case "mysql":
meta, err = parsedsn.MySQL(dsn)
case "postgres":
meta, err = parsedsn.Postgres(dsn)
}
meta = normalize(meta)
return meta, err
}
func normalize(meta map[string]string) map[string]string {
m := make(map[string]string)
for k, v := range meta {
if nk, ok := normalizeKey(k); ok {
m[nk] = v
}
}
return m
}
func normalizeKey(k string) (string, bool) {
switch k {
case "user":
return "db.user", true
case "application_name":
return "db.application", true
case "dbname":
return "db.name", true
case "host", "port":
return "out." + k, true
default:
return "", false
}
}

View file

@ -0,0 +1,44 @@
package sql
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDSN(t *testing.T) {
assert := assert.New(t)
expected := map[string]string{
"db.user": "bob",
"out.host": "1.2.3.4",
"out.port": "5432",
"db.name": "mydb",
}
m, err := parseDSN("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
expected = map[string]string{
"db.user": "bob",
"out.host": "1.2.3.4",
"out.port": "5432",
"db.name": "mydb",
}
m, err = parseDSN("mysql", "bob:secret@tcp(1.2.3.4:5432)/mydb")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
expected = map[string]string{
"out.port": "5433",
"out.host": "master-db-master-active.postgres.service.consul",
"db.name": "dogdatastaging",
"db.application": "trace-api",
"db.user": "dog",
}
dsn := "connect_timeout=0 binary_parameters=no password=zMWmQz26GORmgVVKEbEl dbname=dogdatastaging application_name=trace-api port=5433 sslmode=disable host=master-db-master-active.postgres.service.consul user=dog"
m, err = parseDSN("postgres", dsn)
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
}

View file

@ -0,0 +1,25 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const defaultCollation = "utf8_general_ci"
// A blacklist of collations which is unsafe to interpolate parameters.
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
var unsafeCollations = map[string]bool{
"big5_chinese_ci": true,
"sjis_japanese_ci": true,
"gbk_chinese_ci": true,
"big5_bin": true,
"gb2312_bin": true,
"gbk_bin": true,
"sjis_bin": true,
"cp932_japanese_ci": true,
"cp932_bin": true,
}

View file

@ -0,0 +1,148 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"crypto/tls"
"errors"
"strings"
"time"
)
var (
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
)
// Config is a configuration parsed from a DSN string
type Config struct {
User string // Username
Passwd string // Password (requires User)
Net string // Network type
Addr string // Network address (requires Net)
DBName string // Database name
Params map[string]string // Connection parameters
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
TLSConfig string // TLS configuration name
tls *tls.Config // TLS configuration
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
AllowCleartextPasswords bool // Allows the cleartext client side plugin
AllowNativePasswords bool // Allows the native password authentication method
AllowOldPasswords bool // Allows the old insecure password method
ClientFoundRows bool // Return number of matching rows instead of rows changed
ColumnsWithAlias bool // Prepend table alias to column names
InterpolateParams bool // Interpolate placeholders into query string
MultiStatements bool // Allow multiple statements in one query
ParseTime bool // Parse time values to time.Time
Strict bool // Return warnings as errors
}
// ParseDSN parses the DSN string to a Config
func ParseDSN(dsn string) (cfg *Config, err error) {
// New config with some default values
cfg = &Config{
Loc: time.UTC,
Collation: defaultCollation,
}
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find the last '/' (since the password or the net addr might contain a '/')
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] == '/' {
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// [username[:password]@][protocol[(address)]]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the first ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
cfg.Passwd = dsn[k+1 : j]
break
}
}
cfg.User = dsn[:k]
break
}
}
// [protocol[(address)]]
// Find the first '(' in dsn[j+1:i]
for k = j + 1; k < i; k++ {
if dsn[k] == '(' {
// dsn[i-1] must be == ')' if an address is specified
if dsn[i-1] != ')' {
if strings.ContainsRune(dsn[k+1:i], ')') {
return nil, errInvalidDSNUnescaped
}
return nil, errInvalidDSNAddr
}
cfg.Addr = dsn[k+1 : i-1]
break
}
}
cfg.Net = dsn[j+1 : k]
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
break
}
}
cfg.DBName = dsn[i+1 : j]
break
}
}
if !foundSlash && len(dsn) > 0 {
return nil, errInvalidDSNNoSlash
}
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
return nil, errInvalidDSNUnsafeCollation
}
// Set default network if empty
if cfg.Net == "" {
cfg.Net = "tcp"
}
// Set default address if empty
if cfg.Addr == "" {
switch cfg.Net {
case "tcp":
cfg.Addr = "127.0.0.1:3306"
case "unix":
cfg.Addr = "/tmp/mysql.sock"
default:
return nil, errors.New("default addr for network '" + cfg.Net + "' unknown")
}
}
return
}

View file

@ -0,0 +1,3 @@
// Package mysql is the minimal fork of go-sql-driver/mysql so we can use their code
// to parse the mysql DSNs
package mysql

View file

@ -0,0 +1,23 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
// Returns the bool value of the input.
// The 2nd return value indicates if the input was a valid bool value
func readBool(input string) (value bool, valid bool) {
switch input {
case "1", "true", "TRUE", "True":
return true, true
case "0", "false", "FALSE", "False":
return false, true
}
// Not a valid bool value
return
}

View file

@ -0,0 +1,47 @@
// Package parsedsn provides functions to parse any kind of DSNs into a map[string]string
package parsedsn
import (
"strings"
"github.com/DataDog/dd-trace-go/contrib/database/sql/parsedsn/mysql"
"github.com/DataDog/dd-trace-go/contrib/database/sql/parsedsn/pq"
)
// Postgres parses a postgres-type dsn into a map
func Postgres(dsn string) (map[string]string, error) {
var err error
meta := make(map[string]string)
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
dsn, err = pq.ParseURL(dsn)
if err != nil {
return nil, err
}
}
if err := pq.ParseOpts(dsn, meta); err != nil {
return nil, err
}
// Assure that we do not pass the user secret
delete(meta, "password")
return meta, nil
}
// MySQL parses a mysql-type dsn into a map
func MySQL(dsn string) (m map[string]string, err error) {
var cfg *mysql.Config
if cfg, err = mysql.ParseDSN(dsn); err == nil {
addr := strings.Split(cfg.Addr, ":")
m = map[string]string{
"user": cfg.User,
"host": addr[0],
"port": addr[1],
"dbname": cfg.DBName,
}
return m, nil
}
return nil, err
}

View file

@ -0,0 +1,49 @@
package parsedsn
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMySQL(t *testing.T) {
assert := assert.New(t)
expected := map[string]string{
"user": "bob",
"host": "1.2.3.4",
"port": "5432",
"dbname": "mydb",
}
m, err := MySQL("bob:secret@tcp(1.2.3.4:5432)/mydb")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
}
func TestPostgres(t *testing.T) {
assert := assert.New(t)
expected := map[string]string{
"user": "bob",
"host": "1.2.3.4",
"port": "5432",
"dbname": "mydb",
"sslmode": "verify-full",
}
m, err := Postgres("postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
expected = map[string]string{
"user": "dog",
"port": "5433",
"host": "master-db-master-active.postgres.service.consul",
"dbname": "dogdatastaging",
"application_name": "trace-api",
}
dsn := "password=zMWmQz26GORmgVVKEbEl dbname=dogdatastaging application_name=trace-api port=5433 host=master-db-master-active.postgres.service.consul user=dog"
m, err = Postgres(dsn)
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
}

View file

@ -0,0 +1,118 @@
package pq
import (
"fmt"
"unicode"
)
type values map[string]string
// scanner implements a tokenizer for libpq-style option strings.
type scanner struct {
s []rune
i int
}
// newScanner returns a new scanner initialized with the option string s.
func newScanner(s string) *scanner {
return &scanner{[]rune(s), 0}
}
// Next returns the next rune.
// It returns 0, false if the end of the text has been reached.
func (s *scanner) Next() (rune, bool) {
if s.i >= len(s.s) {
return 0, false
}
r := s.s[s.i]
s.i++
return r, true
}
// SkipSpaces returns the next non-whitespace rune.
// It returns 0, false if the end of the text has been reached.
func (s *scanner) SkipSpaces() (rune, bool) {
r, ok := s.Next()
for unicode.IsSpace(r) && ok {
r, ok = s.Next()
}
return r, ok
}
// ParseOpts parses the options from name and adds them to the values.
// The parsing code is based on conninfo_parse from libpq's fe-connect.c
func ParseOpts(name string, o values) error {
s := newScanner(name)
for {
var (
keyRunes, valRunes []rune
r rune
ok bool
)
if r, ok = s.SkipSpaces(); !ok {
break
}
// Scan the key
for !unicode.IsSpace(r) && r != '=' {
keyRunes = append(keyRunes, r)
if r, ok = s.Next(); !ok {
break
}
}
// Skip any whitespace if we're not at the = yet
if r != '=' {
r, ok = s.SkipSpaces()
}
// The current character should be =
if r != '=' || !ok {
return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes))
}
// Skip any whitespace after the =
if r, ok = s.SkipSpaces(); !ok {
// If we reach the end here, the last value is just an empty string as per libpq.
o[string(keyRunes)] = ""
break
}
if r != '\'' {
for !unicode.IsSpace(r) {
if r == '\\' {
if r, ok = s.Next(); !ok {
return fmt.Errorf(`missing character after backslash`)
}
}
valRunes = append(valRunes, r)
if r, ok = s.Next(); !ok {
break
}
}
} else {
quote:
for {
if r, ok = s.Next(); !ok {
return fmt.Errorf(`unterminated quoted string literal in connection string`)
}
switch r {
case '\'':
break quote
case '\\':
r, _ = s.Next()
fallthrough
default:
valRunes = append(valRunes, r)
}
}
}
o[string(keyRunes)] = string(valRunes)
}
return nil
}

View file

@ -0,0 +1,2 @@
// Package pq is the minimal fork of lib/pq so we can use their code to parse the postgres DSNs
package pq

View file

@ -0,0 +1,76 @@
package pq
import (
"fmt"
"net"
nurl "net/url"
"sort"
"strings"
)
// ParseURL no longer needs to be used by clients of this library since supplying a URL as a
// connection string to sql.Open() is now supported:
//
// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
//
// It remains exported here for backwards-compatibility.
//
// ParseURL converts a url to a connection string for driver.Open.
// Example:
//
// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full"
//
// converts to:
//
// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full"
//
// A minimal example:
//
// "postgres://"
//
// This will be blank, causing driver.Open to use all of the defaults
func ParseURL(url string) (string, error) {
u, err := nurl.Parse(url)
if err != nil {
return "", err
}
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
}
var kvs []string
escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`)
accrue := func(k, v string) {
if v != "" {
kvs = append(kvs, k+"="+escaper.Replace(v))
}
}
if u.User != nil {
v := u.User.Username()
accrue("user", v)
v, _ = u.User.Password()
accrue("password", v)
}
if host, port, err := net.SplitHostPort(u.Host); err != nil {
accrue("host", u.Host)
} else {
accrue("host", host)
accrue("port", port)
}
if u.Path != "" {
accrue("dbname", u.Path[1:])
}
q := u.Query()
for k := range q {
accrue(k, q.Get(k))
}
sort.Strings(kvs) // Makes testing easier (not a performance concern)
return strings.Join(kvs, " "), nil
}

View file

@ -0,0 +1,41 @@
package sql
import (
"log"
"testing"
"github.com/DataDog/dd-trace-go/contrib/database/sql/sqltest"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/lib/pq"
)
func TestPostgres(t *testing.T) {
trc, transport := tracertest.GetTestTracer()
db, err := OpenTraced(&pq.Driver{}, "postgres://postgres:postgres@127.0.0.1:55432/postgres?sslmode=disable", "postgres-test", trc)
if err != nil {
log.Fatal(err)
}
defer db.Close()
testDB := &sqltest.DB{
DB: db,
Tracer: trc,
Transport: transport,
DriverName: "postgres",
}
expectedSpan := &tracer.Span{
Name: "postgres.query",
Service: "postgres-test",
Type: "sql",
}
expectedSpan.Meta = map[string]string{
"db.user": "postgres",
"out.host": "127.0.0.1",
"out.port": "55432",
"db.name": "postgres",
}
sqltest.AllSQLTests(t, testDB, expectedSpan)
}

View file

@ -0,0 +1,384 @@
// Package sqltraced provides a traced version of any driver implementing the database/sql/driver interface.
// To trace jmoiron/sqlx, see https://godoc.org/github.com/DataDog/dd-trace-go/tracer/contrib/sqlxtraced.
package sql
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
log "github.com/cihub/seelog"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/sqlutils"
"github.com/DataDog/dd-trace-go/tracer/ext"
)
// OpenTraced will first register the traced version of the `driver` if not yet registered and will then open a connection with it.
// This is usually the only function to use when there is no need for the granularity offered by Register and Open.
// The last parameter is optional and enables you to use a custom tracer.
func OpenTraced(driver driver.Driver, dataSourceName, service string, trcv ...*tracer.Tracer) (*sql.DB, error) {
driverName := sqlutils.GetDriverName(driver)
Register(driverName, driver, trcv...)
return Open(driverName, dataSourceName, service)
}
// Register takes a driver and registers a traced version of this one.
// The last parameter is optional and enables you to use a custom tracer.
func Register(driverName string, driver driver.Driver, trcv ...*tracer.Tracer) {
if driver == nil {
log.Error("RegisterTracedDriver: driver is nil")
return
}
var trc *tracer.Tracer
if len(trcv) == 0 || (len(trcv) > 0 && trcv[0] == nil) {
trc = tracer.DefaultTracer
} else {
trc = trcv[0]
}
tracedDriverName := sqlutils.GetTracedDriverName(driverName)
if !stringInSlice(sql.Drivers(), tracedDriverName) {
td := tracedDriver{
Driver: driver,
tracer: trc,
driverName: driverName,
}
sql.Register(tracedDriverName, td)
log.Infof("Register %s driver", tracedDriverName)
} else {
log.Warnf("RegisterTracedDriver: %s already registered", tracedDriverName)
}
}
// Open extends the usual API of sql.Open so you can specify the name of the service
// under which the traces will appear in the datadog app.
func Open(driverName, dataSourceName, service string) (*sql.DB, error) {
tracedDriverName := sqlutils.GetTracedDriverName(driverName)
// The service is passed through the DSN
dsnAndService := newDSNAndService(dataSourceName, service)
return sql.Open(tracedDriverName, dsnAndService)
}
// tracedDriver is a driver we use as a middleware between the database/sql package
// and the driver chosen (e.g. mysql, postgresql...).
// It implements the driver.Driver interface and add the tracing features on top
// of the driver's methods.
type tracedDriver struct {
driver.Driver
tracer *tracer.Tracer
driverName string
}
// Open returns a tracedConn so that we can pass all the info we get from the DSN
// all along the tracing
func (td tracedDriver) Open(dsnAndService string) (c driver.Conn, err error) {
var meta map[string]string
var conn driver.Conn
dsn, service := parseDSNAndService(dsnAndService)
// Register the service to Datadog tracing API
td.tracer.SetServiceInfo(service, td.driverName, ext.AppTypeDB)
// Get all kinds of information from the DSN
meta, err = parseDSN(td.driverName, dsn)
if err != nil {
return nil, err
}
conn, err = td.Driver.Open(dsn)
if err != nil {
return nil, err
}
ti := traceInfo{
tracer: td.tracer,
driverName: td.driverName,
service: service,
meta: meta,
}
return &tracedConn{conn, ti}, err
}
// traceInfo stores all information relative to the tracing
type traceInfo struct {
tracer *tracer.Tracer
driverName string
service string
resource string
meta map[string]string
}
func (ti traceInfo) getSpan(ctx context.Context, resource string, query ...string) *tracer.Span {
name := fmt.Sprintf("%s.%s", ti.driverName, "query")
span := ti.tracer.NewChildSpanFromContext(name, ctx)
span.Type = ext.SQLType
span.Service = ti.service
span.Resource = resource
if len(query) > 0 {
span.Resource = query[0]
span.SetMeta(ext.SQLQuery, query[0])
}
for k, v := range ti.meta {
span.SetMeta(k, v)
}
return span
}
type tracedConn struct {
driver.Conn
traceInfo
}
func (tc tracedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
span := tc.getSpan(ctx, "Begin")
defer func() {
span.SetError(err)
span.Finish()
}()
if connBeginTx, ok := tc.Conn.(driver.ConnBeginTx); ok {
tx, err = connBeginTx.BeginTx(ctx, opts)
if err != nil {
return nil, err
}
return tracedTx{tx, tc.traceInfo, ctx}, nil
}
tx, err = tc.Conn.Begin()
if err != nil {
return nil, err
}
return tracedTx{tx, tc.traceInfo, ctx}, nil
}
func (tc tracedConn) PrepareContext(ctx context.Context, query string) (stmt driver.Stmt, err error) {
span := tc.getSpan(ctx, "Prepare", query)
defer func() {
span.SetError(err)
span.Finish()
}()
// Check if the driver implements PrepareContext
if connPrepareCtx, ok := tc.Conn.(driver.ConnPrepareContext); ok {
stmt, err := connPrepareCtx.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
return tracedStmt{stmt, tc.traceInfo, ctx, query}, nil
}
// If the driver does not implement PrepareContex (lib/pq for example)
stmt, err = tc.Prepare(query)
if err != nil {
return nil, err
}
return tracedStmt{stmt, tc.traceInfo, ctx, query}, nil
}
func (tc tracedConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if execer, ok := tc.Conn.(driver.Execer); ok {
return execer.Exec(query, args)
}
return nil, driver.ErrSkip
}
func (tc tracedConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (r driver.Result, err error) {
span := tc.getSpan(ctx, "Exec", query)
defer func() {
span.SetError(err)
span.Finish()
}()
if execContext, ok := tc.Conn.(driver.ExecerContext); ok {
res, err := execContext.ExecContext(ctx, query, args)
if err != nil {
return nil, err
}
return res, nil
}
// Fallback implementation
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}
return tc.Exec(query, dargs)
}
// tracedConn has a Ping method in order to implement the pinger interface
func (tc tracedConn) Ping(ctx context.Context) (err error) {
span := tc.getSpan(ctx, "Ping")
defer func() {
span.SetError(err)
span.Finish()
}()
if pinger, ok := tc.Conn.(driver.Pinger); ok {
err = pinger.Ping(ctx)
}
return err
}
func (tc tracedConn) Query(query string, args []driver.Value) (driver.Rows, error) {
if queryer, ok := tc.Conn.(driver.Queryer); ok {
return queryer.Query(query, args)
}
return nil, driver.ErrSkip
}
func (tc tracedConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {
span := tc.getSpan(ctx, "Query", query)
defer func() {
span.SetError(err)
span.Finish()
}()
if queryerContext, ok := tc.Conn.(driver.QueryerContext); ok {
rows, err := queryerContext.QueryContext(ctx, query, args)
if err != nil {
return nil, err
}
return rows, nil
}
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}
return tc.Query(query, dargs)
}
// tracedTx is a traced version of sql.Tx
type tracedTx struct {
driver.Tx
traceInfo
ctx context.Context
}
// Commit sends a span at the end of the transaction
func (t tracedTx) Commit() (err error) {
span := t.getSpan(t.ctx, "Commit")
defer func() {
span.SetError(err)
span.Finish()
}()
return t.Tx.Commit()
}
// Rollback sends a span if the connection is aborted
func (t tracedTx) Rollback() (err error) {
span := t.getSpan(t.ctx, "Rollback")
defer func() {
span.SetError(err)
span.Finish()
}()
return t.Tx.Rollback()
}
// tracedStmt is traced version of sql.Stmt
type tracedStmt struct {
driver.Stmt
traceInfo
ctx context.Context
query string
}
// Close sends a span before closing a statement
func (s tracedStmt) Close() (err error) {
span := s.getSpan(s.ctx, "Close")
defer func() {
span.SetError(err)
span.Finish()
}()
return s.Stmt.Close()
}
// ExecContext is needed to implement the driver.StmtExecContext interface
func (s tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
span := s.getSpan(s.ctx, "Exec", s.query)
defer func() {
span.SetError(err)
span.Finish()
}()
if stmtExecContext, ok := s.Stmt.(driver.StmtExecContext); ok {
res, err = stmtExecContext.ExecContext(ctx, args)
if err != nil {
return nil, err
}
return res, nil
}
// Fallback implementation
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}
return s.Exec(dargs)
}
// QueryContext is needed to implement the driver.StmtQueryContext interface
func (s tracedStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
span := s.getSpan(s.ctx, "Query", s.query)
defer func() {
span.SetError(err)
span.Finish()
}()
if stmtQueryContext, ok := s.Stmt.(driver.StmtQueryContext); ok {
rows, err = stmtQueryContext.QueryContext(ctx, args)
if err != nil {
return nil, err
}
return rows, nil
}
// Fallback implementation
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}
return s.Query(dargs)
}

View file

@ -0,0 +1,211 @@
// Package sqltest is used for testing sql packages
package sqltest
import (
"context"
"database/sql"
"fmt"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/stretchr/testify/assert"
)
// setupTestCase initializes MySQL or Postgres databases and returns a
// teardown function that must be executed via `defer`
func setupTestCase(t *testing.T, db *DB) func(t *testing.T, db *DB) {
// creates the database
db.Exec("DROP TABLE IF EXISTS city")
db.Exec("CREATE TABLE city (id integer NOT NULL DEFAULT '0', name text)")
// Empty the tracer
db.Tracer.ForceFlush()
db.Transport.Traces()
return func(t *testing.T, db *DB) {
// drop the table
db.Exec("DROP TABLE city")
}
}
// AllSQLTests applies a sequence of unit tests to check the correct tracing of sql features.
func AllSQLTests(t *testing.T, db *DB, expectedSpan *tracer.Span) {
// database setup and cleanup
tearDown := setupTestCase(t, db)
defer tearDown(t, db)
testDB(t, db, expectedSpan)
testStatement(t, db, expectedSpan)
testTransaction(t, db, expectedSpan)
}
func testDB(t *testing.T, db *DB, expectedSpan *tracer.Span) {
assert := assert.New(t)
const query = "SELECT id, name FROM city LIMIT 5"
// Test db.Ping
err := db.Ping()
assert.Equal(nil, err)
db.Tracer.ForceFlush()
traces := db.Transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
actualSpan := spans[0]
pingSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
pingSpan.Resource = "Ping"
tracertest.CompareSpan(t, pingSpan, actualSpan)
// Test db.Query
rows, err := db.Query(query)
defer rows.Close()
assert.Equal(nil, err)
db.Tracer.ForceFlush()
traces = db.Transport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 1)
actualSpan = spans[0]
querySpan := tracertest.CopySpan(expectedSpan, db.Tracer)
querySpan.Resource = query
querySpan.SetMeta("sql.query", query)
tracertest.CompareSpan(t, querySpan, actualSpan)
delete(expectedSpan.Meta, "sql.query")
}
func testStatement(t *testing.T, db *DB, expectedSpan *tracer.Span) {
assert := assert.New(t)
query := "INSERT INTO city(name) VALUES(%s)"
switch db.DriverName {
case "postgres":
query = fmt.Sprintf(query, "$1")
case "mysql":
query = fmt.Sprintf(query, "?")
}
// Test TracedConn.PrepareContext
stmt, err := db.Prepare(query)
assert.Equal(nil, err)
db.Tracer.ForceFlush()
traces := db.Transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
actualSpan := spans[0]
prepareSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
prepareSpan.Resource = query
prepareSpan.SetMeta("sql.query", query)
tracertest.CompareSpan(t, prepareSpan, actualSpan)
delete(expectedSpan.Meta, "sql.query")
// Test Exec
_, err2 := stmt.Exec("New York")
assert.Equal(nil, err2)
db.Tracer.ForceFlush()
traces = db.Transport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 1)
actualSpan = spans[0]
execSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
execSpan.Resource = query
execSpan.SetMeta("sql.query", query)
tracertest.CompareSpan(t, execSpan, actualSpan)
delete(expectedSpan.Meta, "sql.query")
}
func testTransaction(t *testing.T, db *DB, expectedSpan *tracer.Span) {
assert := assert.New(t)
query := "INSERT INTO city(name) VALUES('New York')"
// Test Begin
tx, err := db.Begin()
assert.Equal(nil, err)
db.Tracer.ForceFlush()
traces := db.Transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
actualSpan := spans[0]
beginSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
beginSpan.Resource = "Begin"
tracertest.CompareSpan(t, beginSpan, actualSpan)
// Test Rollback
err = tx.Rollback()
assert.Equal(nil, err)
db.Tracer.ForceFlush()
traces = db.Transport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 1)
actualSpan = spans[0]
rollbackSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
rollbackSpan.Resource = "Rollback"
tracertest.CompareSpan(t, rollbackSpan, actualSpan)
// Test Exec
parentSpan := db.Tracer.NewRootSpan("test.parent", "test", "parent")
ctx := tracer.ContextWithSpan(context.Background(), parentSpan)
tx, err = db.BeginTx(ctx, nil)
assert.Equal(nil, err)
_, err = tx.ExecContext(ctx, query)
assert.Equal(nil, err)
err = tx.Commit()
assert.Equal(nil, err)
parentSpan.Finish() // need to do this else children are not flushed at all
db.Tracer.ForceFlush()
traces = db.Transport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 4)
for _, s := range spans {
if s.Name == expectedSpan.Name && s.Resource == query {
actualSpan = s
}
}
assert.NotNil(actualSpan)
execSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
execSpan.Resource = query
execSpan.SetMeta("sql.query", query)
tracertest.CompareSpan(t, execSpan, actualSpan)
delete(expectedSpan.Meta, "sql.query")
for _, s := range spans {
if s.Name == expectedSpan.Name && s.Resource == "Commit" {
actualSpan = s
}
}
assert.NotNil(actualSpan)
commitSpan := tracertest.CopySpan(expectedSpan, db.Tracer)
commitSpan.Resource = "Commit"
tracertest.CompareSpan(t, commitSpan, actualSpan)
}
// DB is a struct dedicated for testing
type DB struct {
*sql.DB
Tracer *tracer.Tracer
Transport *tracertest.DummyTransport
DriverName string
}

View file

@ -0,0 +1,2 @@
// Package sqlutils share some utils functions for sql packages
package sqlutils

View file

@ -0,0 +1,59 @@
package sqlutils
import (
"database/sql/driver"
"errors"
"fmt"
"reflect"
"sort"
"strings"
)
// GetDriverName returns the driver type.
func GetDriverName(driver driver.Driver) string {
if driver == nil {
return ""
}
driverType := fmt.Sprintf("%s", reflect.TypeOf(driver))
switch driverType {
case "*mysql.MySQLDriver":
return "mysql"
case "*pq.Driver":
return "postgres"
default:
return ""
}
}
// GetTracedDriverName add the suffix "Traced" to the driver name.
func GetTracedDriverName(driverName string) string {
return driverName + "Traced"
}
func newDSNAndService(dsn, service string) string {
return dsn + "|" + service
}
func parseDSNAndService(dsnAndService string) (dsn, service string) {
tab := strings.Split(dsnAndService, "|")
return tab[0], tab[1]
}
// namedValueToValue is a helper function copied from the database/sql package.
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
dargs := make([]driver.Value, len(named))
for n, param := range named {
if len(param.Name) > 0 {
return nil, errors.New("sql: driver does not support the use of Named Parameters")
}
dargs[n] = param.Value
}
return dargs, nil
}
// stringInSlice returns true if the string s is in the list.
func stringInSlice(list []string, s string) bool {
sort.Strings(list)
i := sort.SearchStrings(list, s)
return i < len(list) && list[i] == s
}

View file

@ -0,0 +1,17 @@
package sqlutils
import (
"testing"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
)
func TestGetDriverName(t *testing.T) {
assert := assert.New(t)
assert.Equal("postgres", GetDriverName(&pq.Driver{}))
assert.Equal("mysql", GetDriverName(&mysql.MySQLDriver{}))
assert.Equal("", GetDriverName(nil))
}

View file

@ -0,0 +1,36 @@
package sql
import (
"database/sql/driver"
"errors"
"sort"
"strings"
)
func newDSNAndService(dsn, service string) string {
return dsn + "|" + service
}
func parseDSNAndService(dsnAndService string) (dsn, service string) {
tab := strings.Split(dsnAndService, "|")
return tab[0], tab[1]
}
// namedValueToValue is a helper function copied from the database/sql package.
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
dargs := make([]driver.Value, len(named))
for n, param := range named {
if len(param.Name) > 0 {
return nil, errors.New("sql: driver does not support the use of Named Parameters")
}
dargs[n] = param.Value
}
return dargs, nil
}
// stringInSlice returns true if the string s is in the list.
func stringInSlice(list []string, s string) bool {
sort.Strings(list)
i := sort.SearchStrings(list, s)
return i < len(list) && list[i] == s
}

View file

@ -0,0 +1,29 @@
package sql
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStringInSlice(t *testing.T) {
assert := assert.New(t)
list := []string{"mysql", "postgres", "pq"}
assert.True(stringInSlice(list, "pq"))
assert.False(stringInSlice(list, "Postgres"))
}
func TestDSNAndService(t *testing.T) {
assert := assert.New(t)
dsn := "postgres://ubuntu@127.0.0.1:5432/circle_test?sslmode=disable"
service := "master-db"
dsnAndService := "postgres://ubuntu@127.0.0.1:5432/circle_test?sslmode=disable|master-db"
assert.Equal(dsnAndService, newDSNAndService(dsn, service))
actualDSN, actualService := parseDSNAndService(dsnAndService)
assert.Equal(dsn, actualDSN)
assert.Equal(service, actualService)
}

View file

@ -0,0 +1,59 @@
package redigo_test
import (
"context"
redigotrace "github.com/DataDog/dd-trace-go/contrib/garyburd/redigo"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/garyburd/redigo/redis"
)
// To start tracing Redis commands, use the TracedDial function to create a connection,
// passing in a service name of choice.
func Example() {
c, _ := redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
// Emit spans per command by using your Redis connection as usual
c.Do("SET", "vehicle", "truck")
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When passed a context as the final argument, c.Do will emit a span inheriting from 'parent.request'
c.Do("SET", "food", "cheese", ctx)
root.Finish()
}
func ExampleTracedConn() {
c, _ := redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
// Emit spans per command by using your Redis connection as usual
c.Do("SET", "vehicle", "truck")
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When passed a context as the final argument, c.Do will emit a span inheriting from 'parent.request'
c.Do("SET", "food", "cheese", ctx)
root.Finish()
}
// Alternatively, provide a redis URL to the TracedDialURL function
func Example_dialURL() {
c, _ := redigotrace.TracedDialURL("my-redis-backend", tracer.DefaultTracer, "redis://127.0.0.1:6379")
c.Do("SET", "vehicle", "truck")
}
// When using a redigo Pool, set your Dial function to return a traced connection
func Example_pool() {
pool := &redis.Pool{
Dial: func() (redis.Conn, error) {
return redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
},
}
c := pool.Get()
c.Do("SET", " whiskey", " glass")
}

View file

@ -0,0 +1,131 @@
// Package redigo provides tracing for the Redigo Redis client (https://github.com/garyburd/redigo)
package redigo
import (
"bytes"
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
redis "github.com/garyburd/redigo/redis"
"net"
"net/url"
"strconv"
"strings"
)
// TracedConn is an implementation of the redis.Conn interface that supports tracing
type TracedConn struct {
redis.Conn
p traceParams
}
// traceParams contains fields and metadata useful for command tracing
type traceParams struct {
tracer *tracer.Tracer
service string
network string
host string
port string
}
// TracedDial takes a Conn returned by redis.Dial and configures it to emit spans with the given service name
func TracedDial(service string, tracer *tracer.Tracer, network, address string, options ...redis.DialOption) (redis.Conn, error) {
c, err := redis.Dial(network, address, options...)
addr := strings.Split(address, ":")
var host, port string
if len(addr) == 2 && addr[1] != "" {
port = addr[1]
} else {
port = "6379"
}
host = addr[0]
tracer.SetServiceInfo(service, "redis", ext.AppTypeDB)
tc := TracedConn{c, traceParams{tracer, service, network, host, port}}
return tc, err
}
// TracedDialURL takes a Conn returned by redis.DialURL and configures it to emit spans with the given service name
func TracedDialURL(service string, tracer *tracer.Tracer, rawurl string, options ...redis.DialOption) (redis.Conn, error) {
u, err := url.Parse(rawurl)
if err != nil {
return TracedConn{}, err
}
// Getting host and port, usind code from https://github.com/garyburd/redigo/blob/master/redis/conn.go#L226
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Host
port = "6379"
}
if host == "" {
host = "localhost"
}
// Set in redis.DialUrl source code
network := "tcp"
c, err := redis.DialURL(rawurl, options...)
tc := TracedConn{c, traceParams{tracer, service, 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 TracedConn) NewChildSpan(ctx context.Context) *tracer.Span {
span := tc.p.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = tc.p.service
span.SetMeta("out.network", tc.p.network)
span.SetMeta("out.port", tc.p.port)
span.SetMeta("out.host", tc.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 TracedConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
var ctx context.Context
var ok bool
if len(args) > 0 {
ctx, ok = args[len(args)-1].(context.Context)
if ok {
args = args[:len(args)-1]
}
}
span := tc.NewChildSpan(ctx)
defer func() {
if err != nil {
span.SetError(err)
}
span.Finish()
}()
span.SetMeta("redis.args_length", strconv.Itoa(len(args)))
if len(commandName) > 0 {
span.Resource = 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.Resource = "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.SetMeta("redis.raw_command", b.String())
return tc.Conn.Do(commandName, args...)
}

View file

@ -0,0 +1,214 @@
package redigo
import (
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/garyburd/redigo/redis"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
const (
debug = false
)
func TestClient(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
c.Do("SET", 1, "truck")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "SET")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "SET 1 truck")
assert.Equal(span.GetMeta("redis.args_length"), "2")
}
func TestCommandError(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
_, err := c.Do("NOT_A_COMMAND", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Error())
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "NOT_A_COMMAND")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "NOT_A_COMMAND")
}
func TestConnectionError(t *testing.T) {
assert := assert.New(t)
testTracer, _ := getTestTracer()
testTracer.SetDebugLogging(debug)
_, err := TracedDial("redis-service", testTracer, "tcp", "127.0.0.1:1000")
assert.Contains(err.Error(), "dial tcp 127.0.0.1:1000")
}
func TestInheritance(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parent_span := testTracer.NewChildSpanFromContext("parent_span", ctx)
ctx = tracer.ContextWithSpan(ctx, parent_span)
client, _ := TracedDial("my_service", testTracer, "tcp", "127.0.0.1:56379")
client.Do("SET", "water", "bottle", ctx)
parent_span.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var child_span, pspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "redis.command":
child_span = s
case "parent_span":
pspan = s
}
}
assert.NotNil(child_span, "there should be a child redis.command span")
assert.NotNil(child_span, "there should be a parent span")
assert.Equal(child_span.ParentID, pspan.SpanID)
assert.Equal(child_span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(child_span.GetMeta("out.port"), "56379")
}
func TestCommandsToSring(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
stringify_test := TestStruct{Cpython: 57, Cgo: 8}
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
c.Do("SADD", "testSet", "a", int(0), int32(1), int64(2), stringify_test, context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "SADD")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "SADD testSet a 0 1 2 [57, 8]")
}
func TestPool(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
pool := &redis.Pool{
MaxIdle: 2,
MaxActive: 3,
IdleTimeout: 23,
Wait: true,
Dial: func() (redis.Conn, error) {
return TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
},
}
pc := pool.Get()
pc.Do("SET", " whiskey", " glass", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.GetMeta("out.network"), "tcp")
}
func TestTracingDialUrl(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
url := "redis://127.0.0.1:56379"
client, _ := TracedDialURL("redis-service", testTracer, url)
client.Do("SET", "ONE", " TWO", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
}
// TestStruct implements String interface
type TestStruct struct {
Cpython int
Cgo int
}
func (ts TestStruct) String() string {
return fmt.Sprintf("[%d, %d]", ts.Cpython, ts.Cgo)
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,58 @@
package gin_test
import (
gintrace "github.com/DataDog/dd-trace-go/contrib/gin-gonic/gin"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gin-gonic/gin"
)
// To start tracing requests, add the trace middleware to your Gin router.
func Example() {
// Create your router and use the middleware.
r := gin.New()
r.Use(gintrace.Middleware("my-web-app"))
r.GET("/hello", func(c *gin.Context) {
c.String(200, "hello world!")
})
// Profit!
r.Run(":8080")
}
func ExampleHTML() {
r := gin.Default()
r.Use(gintrace.Middleware("my-web-app"))
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
// This will render the html and trace the execution time.
gintrace.HTML(c, 200, "index.tmpl", gin.H{
"title": "Main website",
})
})
}
func ExampleSpanDefault() {
r := gin.Default()
r.Use(gintrace.Middleware("image-encoder"))
r.GET("/image/encode", func(c *gin.Context) {
// The middleware patches a span to the request. Let's add some metadata,
// and create a child span.
span := gintrace.SpanDefault(c)
span.SetMeta("user.handle", "admin")
span.SetMeta("user.id", "1234")
encodeSpan := tracer.NewChildSpan("image.encode", span)
// encode a image
encodeSpan.Finish()
uploadSpan := tracer.NewChildSpan("image.upload", span)
// upload the image
uploadSpan.Finish()
c.String(200, "ok!")
})
}

View file

@ -0,0 +1,143 @@
// Package gin provides tracing middleware for the Gin web framework.
package gin
import (
"fmt"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gin-gonic/gin"
)
// key is the string that we'll use to store spans in the tracer.
var key = "datadog_trace_span"
// Middleware returns middleware that will trace requests with the default
// tracer.
func Middleware(service string) gin.HandlerFunc {
return MiddlewareTracer(service, tracer.DefaultTracer)
}
// MiddlewareTracer returns middleware that will trace requests with the given
// tracer.
func MiddlewareTracer(service string, t *tracer.Tracer) gin.HandlerFunc {
t.SetServiceInfo(service, "gin-gonic", ext.AppTypeWeb)
mw := newMiddleware(service, t)
return mw.Handle
}
// middleware implements gin middleware.
type middleware struct {
service string
trc *tracer.Tracer
}
func newMiddleware(service string, trc *tracer.Tracer) *middleware {
return &middleware{
service: service,
trc: trc,
}
}
// Handle is a gin HandlerFunc that will add tracing to the given request.
func (m *middleware) Handle(c *gin.Context) {
// bail if not enabled
if !m.trc.Enabled() {
c.Next()
return
}
// FIXME[matt] the handler name is a bit unwieldy and uses reflection
// under the hood. might be better to tackle this task and do it right
// so we can end up with "user/:user/whatever" instead of
// "github.com/foobar/blah"
//
// See here: https://github.com/gin-gonic/gin/issues/649
resource := c.HandlerName()
// Create our span and patch it to the context for downstream.
span := m.trc.NewRootSpan("gin.request", m.service, resource)
c.Set(key, span)
// Pass along the request.
c.Next()
// Set http tags.
span.SetMeta(ext.HTTPCode, strconv.Itoa(c.Writer.Status()))
span.SetMeta(ext.HTTPMethod, c.Request.Method)
span.SetMeta(ext.HTTPURL, c.Request.URL.Path)
// Set any error information.
var err error
if len(c.Errors) > 0 {
span.SetMeta("gin.errors", c.Errors.String()) // set all errors
err = c.Errors[0] // but use the first for standard fields
}
span.FinishWithErr(err)
}
// Span returns the Span stored in the given Context and true. If it doesn't exist,
// it will returns (nil, false)
func Span(c *gin.Context) (*tracer.Span, bool) {
if c == nil {
return nil, false
}
s, ok := c.Get(key)
if !ok {
return nil, false
}
switch span := s.(type) {
case *tracer.Span:
return span, true
}
return nil, false
}
// SpanDefault returns the span stored in the given Context. If none exists,
// it will return an empty span.
func SpanDefault(c *gin.Context) *tracer.Span {
span, ok := Span(c)
if !ok {
return &tracer.Span{}
}
return span
}
// NewChildSpan will create a span that is the child of the span stored in
// the context.
func NewChildSpan(name string, c *gin.Context) *tracer.Span {
span, ok := Span(c)
if !ok {
return &tracer.Span{}
}
return span.Tracer().NewChildSpan(name, span)
}
// HTML will trace the rendering of the template as a child of the span in the
// given context.
func HTML(c *gin.Context, code int, name string, obj interface{}) {
span, _ := Span(c)
if span == nil {
c.HTML(code, name, obj)
return
}
child := span.Tracer().NewChildSpan("gin.render.html", span)
child.SetMeta("go.template", name)
defer func() {
if r := recover(); r != nil {
err := fmt.Errorf("error rendering tmpl:%s: %s", name, r)
child.FinishWithErr(err)
panic(r)
} else {
child.Finish()
}
}()
// render
c.HTML(code, name, obj)
}

View file

@ -0,0 +1,250 @@
package gin
import (
"errors"
"fmt"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func init() {
gin.SetMode(gin.ReleaseMode) // silence annoying log msgs
}
func TestChildSpan(t *testing.T) {
assert := assert.New(t)
testTracer, _ := getTestTracer()
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/user/:id", func(c *gin.Context) {
span, ok := tracer.SpanFromContext(c)
assert.True(ok)
assert.NotNil(span)
})
r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
}
func TestTrace200(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/user/:id", func(c *gin.Context) {
// assert we patch the span on the request context.
span := SpanDefault(c)
span.SetMeta("test.gin", "ginny")
assert.Equal(span.Service, "foobar")
id := c.Param("id")
c.Writer.Write([]byte(id))
})
r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
// do and verify the request
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
// verify traces look good
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
if len(spans) < 1 {
t.Fatalf("no spans")
}
s := spans[0]
assert.Equal(s.Service, "foobar")
assert.Equal(s.Name, "gin.request")
// FIXME[matt] would be much nicer to have "/user/:id" here
assert.True(strings.Contains(s.Resource, "gin.TestTrace200"))
assert.Equal(s.GetMeta("test.gin"), "ginny")
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), "/user/123")
}
func TestDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetEnabled(false)
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/ping", func(c *gin.Context) {
span, ok := Span(c)
assert.Nil(span)
assert.False(ok)
c.Writer.Write([]byte("ok"))
})
r := httptest.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
// do and verify the request
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
// verify traces look good
testTracer.ForceFlush()
spans := testTransport.Traces()
assert.Len(spans, 0)
}
func TestError(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
// setup
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
// a handler with an error and make the requests
router.GET("/err", func(c *gin.Context) {
c.AbortWithError(500, errors.New("oh no"))
})
r := httptest.NewRequest("GET", "/err", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 500)
// verify the errors and status are correct
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
if len(spans) < 1 {
t.Fatalf("no spans")
}
s := spans[0]
assert.Equal(s.Service, "foobar")
assert.Equal(s.Name, "gin.request")
assert.Equal(s.GetMeta("http.status_code"), "500")
assert.Equal(s.GetMeta(ext.ErrorMsg), "oh no")
assert.Equal(s.Error, int32(1))
}
func TestHTML(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
// setup
middleware := newMiddleware("tmplservice", testTracer)
router := gin.New()
router.Use(middleware.Handle)
// add a template
tmpl := template.Must(template.New("hello").Parse("hello {{.}}"))
router.SetHTMLTemplate(tmpl)
// a handler with an error and make the requests
router.GET("/hello", func(c *gin.Context) {
HTML(c, 200, "hello", "world")
})
r := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
assert.Equal("hello world", w.Body.String())
// verify the errors and status are correct
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
for _, s := range spans {
assert.Equal(s.Service, "tmplservice")
}
var tspan *tracer.Span
for _, s := range spans {
// we need to pick up the span we're searching for, as the
// order is not garanteed within the buffer
if s.Name == "gin.render.html" {
tspan = s
}
}
assert.NotNil(tspan, "we should have found a span with name gin.render.html")
assert.Equal(tspan.GetMeta("go.template"), "hello")
fmt.Println(spans)
}
func TestGetSpanNotInstrumented(t *testing.T) {
assert := assert.New(t)
router := gin.New()
router.GET("/ping", func(c *gin.Context) {
// Assert we don't have a span on the context.
s, ok := Span(c)
assert.False(ok)
assert.Nil(s)
// and the default span is empty
s = SpanDefault(c)
assert.Equal(s.Service, "")
c.Writer.Write([]byte("ok"))
})
r := httptest.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,81 @@
package redis_test
import (
"context"
"fmt"
redistrace "github.com/DataDog/dd-trace-go/contrib/go-redis/redis"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/gin-gonic/gintrace"
"github.com/gin-gonic/gin"
"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 := redistrace.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 := redistrace.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 := redistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// pipe 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 := redistrace.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()
}

View file

@ -0,0 +1,151 @@
// Package redis provides tracing for the go-redis Redis client (https://github.com/go-redis/redis)
package redis
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
}
}
}

View file

@ -0,0 +1,228 @@
package redis
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/go-redis/redis"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"time"
)
const (
debug = false
)
func TestClient(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default db
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
client.Set("test_key", "test_value", 0)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "set test_key test_value: ")
assert.Equal(span.GetMeta("redis.args_length"), "3")
}
func TestPipeline(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default db
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
pipeline := client.Pipeline()
pipeline.Expire("pipeline_counter", time.Hour)
// Exec with context test
pipeline.ExecWithContext(context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.pipeline_length"), "1")
assert.Equal(span.Resource, "expire pipeline_counter 3600: false\n")
pipeline.Expire("pipeline_counter", time.Hour)
pipeline.Expire("pipeline_counter_1", time.Minute)
// Rewriting Exec
pipeline.Exec()
testTracer.ForceFlush()
traces = testTransport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 1)
span = spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("redis.pipeline_length"), "2")
assert.Equal(span.Resource, "expire pipeline_counter 3600: false\nexpire pipeline_counter_1 60: false\n")
}
func TestChildSpan(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parent_span := testTracer.NewChildSpanFromContext("parent_span", ctx)
ctx = tracer.ContextWithSpan(ctx, parent_span)
client := NewTracedClient(opts, testTracer, "my-redis")
client.SetContext(ctx)
client.Set("test_key", "test_value", 0)
parent_span.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var child_span, pspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "redis.command":
child_span = s
case "parent_span":
pspan = s
}
}
assert.NotNil(child_span, "there should be a child redis.command span")
assert.NotNil(child_span, "there should be a parent span")
assert.Equal(child_span.ParentID, pspan.SpanID)
assert.Equal(child_span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(child_span.GetMeta("out.port"), "56379")
}
func TestMultipleCommands(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
client.Set("test_key", "test_value", 0)
client.Get("test_key")
client.Incr("int_key")
client.ClientList()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 4)
spans := traces[0]
assert.Len(spans, 1)
// Checking all commands were recorded
var commands [4]string
for i := 0; i < 4; i++ {
commands[i] = traces[i][0].GetMeta("redis.raw_command")
}
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) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
err := client.Get("non_existent_key")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Err().Error())
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "get non_existent_key: ")
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,27 @@
package gocql_test
import (
"context"
gocqltrace "github.com/DataDog/dd-trace-go/contrib/gocql/gocql"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gocql/gocql"
)
// To trace Cassandra commands, use our query wrapper TraceQuery.
func Example() {
// Initialise a Cassandra session as usual, create a query.
cluster := gocql.NewCluster("127.0.0.1")
session, _ := cluster.CreateSession()
query := session.Query("CREATE KEYSPACE if not exists trace WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1}")
// Use context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// Wrap the query to trace it and pass the context for inheritance
tracedQuery := gocqltrace.TraceQuery("ServiceName", tracer.DefaultTracer, query)
tracedQuery.WithContext(ctx)
// Execute your query as usual
tracedQuery.Exec()
}

View file

@ -0,0 +1,146 @@
// Package gocql provides tracing for the Cassandra Gocql client (https://github.com/gocql/gocql)
package gocql
import (
"context"
"strconv"
"strings"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gocql/gocql"
)
// TracedQuery inherits from gocql.Query, it keeps the tracer and the context.
type TracedQuery struct {
*gocql.Query
p traceParams
traceContext context.Context
}
// TracedIter inherits from gocql.Iter and contains a span.
type TracedIter struct {
*gocql.Iter
span *tracer.Span
}
// traceParams containes fields and metadata useful for command tracing
type traceParams struct {
tracer *tracer.Tracer
service string
keyspace string
paginated string
consistancy string
query string
}
// TraceQuery wraps a gocql.Query into a TracedQuery
func TraceQuery(service string, tracer *tracer.Tracer, q *gocql.Query) *TracedQuery {
stringQuery := `"` + strings.SplitN(q.String(), "\"", 3)[1] + `"`
stringQuery, err := strconv.Unquote(stringQuery)
if err != nil {
// An invalid string, so that the trace is not dropped
// due to having an empty resource
stringQuery = "_"
}
tq := &TracedQuery{q, traceParams{tracer, service, "", "false", strconv.Itoa(int(q.GetConsistency())), stringQuery}, context.Background()}
tracer.SetServiceInfo(service, ext.CassandraType, ext.AppTypeDB)
return tq
}
// WithContext rewrites the original function so that ctx can be used for inheritance
func (tq *TracedQuery) WithContext(ctx context.Context) *TracedQuery {
tq.traceContext = ctx
tq.Query.WithContext(ctx)
return tq
}
// PageState rewrites the original function so that spans are aware of the change.
func (tq *TracedQuery) PageState(state []byte) *TracedQuery {
tq.p.paginated = "true"
tq.Query = tq.Query.PageState(state)
return tq
}
// NewChildSpan creates a new span from the traceParams and the context.
func (tq *TracedQuery) NewChildSpan(ctx context.Context) *tracer.Span {
span := tq.p.tracer.NewChildSpanFromContext(ext.CassandraQuery, ctx)
span.Type = ext.CassandraType
span.Service = tq.p.service
span.Resource = tq.p.query
span.SetMeta(ext.CassandraPaginated, tq.p.paginated)
span.SetMeta(ext.CassandraKeyspace, tq.p.keyspace)
return span
}
// Exec is rewritten so that it passes by our custom Iter
func (tq *TracedQuery) Exec() error {
return tq.Iter().Close()
}
// MapScan wraps in a span query.MapScan call.
func (tq *TracedQuery) MapScan(m map[string]interface{}) error {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
err := tq.Query.MapScan(m)
if err != nil {
span.SetError(err)
}
return err
}
// Scan wraps in a span query.Scan call.
func (tq *TracedQuery) Scan(dest ...interface{}) error {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
err := tq.Query.Scan(dest...)
if err != nil {
span.SetError(err)
}
return err
}
// ScanCAS wraps in a span query.ScanCAS call.
func (tq *TracedQuery) ScanCAS(dest ...interface{}) (applied bool, err error) {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
applied, err = tq.Query.ScanCAS(dest...)
if err != nil {
span.SetError(err)
}
return applied, err
}
// Iter starts a new span at query.Iter call.
func (tq *TracedQuery) Iter() *TracedIter {
span := tq.NewChildSpan(tq.traceContext)
iter := tq.Query.Iter()
span.SetMeta(ext.CassandraRowCount, strconv.Itoa(iter.NumRows()))
span.SetMeta(ext.CassandraConsistencyLevel, strconv.Itoa(int(tq.GetConsistency())))
columns := iter.Columns()
if len(columns) > 0 {
span.SetMeta(ext.CassandraKeyspace, columns[0].Keyspace)
} else {
}
tIter := &TracedIter{iter, span}
if tIter.Host() != nil {
tIter.span.SetMeta(ext.TargetHost, tIter.Iter.Host().HostID())
tIter.span.SetMeta(ext.TargetPort, strconv.Itoa(tIter.Iter.Host().Port()))
tIter.span.SetMeta(ext.CassandraCluster, tIter.Iter.Host().DataCenter())
}
return tIter
}
// Close closes the TracedIter and finish the span created on Iter call.
func (tIter *TracedIter) Close() error {
err := tIter.Iter.Close()
if err != nil {
tIter.span.SetError(err)
}
tIter.span.Finish()
return err
}

View file

@ -0,0 +1,144 @@
package gocql
import (
"context"
"net/http"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
)
const (
debug = false
CASSANDRA_HOST = "127.0.0.1:59042"
)
func newCassandraCluster() *gocql.ClusterConfig {
cluster := gocql.NewCluster(CASSANDRA_HOST)
// the InitialHostLookup must be disabled in newer versions of
// gocql otherwise "no connections were made when creating the session"
// error is returned for Cassandra misconfiguration (that we don't need
// since we're testing another behavior and not the client).
// Check: https://github.com/gocql/gocql/issues/946
cluster.DisableInitialHostLookup = true
return cluster
}
// TestMain sets up the Keyspace and table if they do not exist
func TestMain(m *testing.M) {
cluster := newCassandraCluster()
session, _ := cluster.CreateSession()
// Ensures test keyspace and table person exists.
session.Query("CREATE KEYSPACE if not exists trace WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1}").Exec()
session.Query("CREATE TABLE if not exists trace.person (name text PRIMARY KEY, age int, description text)").Exec()
session.Query("INSERT INTO trace.person (name, age, description) VALUES ('Cassandra', 100, 'A cruel mistress')").Exec()
m.Run()
}
func TestErrorWrapper(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
cluster := newCassandraCluster()
session, _ := cluster.CreateSession()
q := session.Query("CREATE KEYSPACE trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
err := TraceQuery("ServiceName", testTracer, q).Exec()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Error())
assert.Equal(span.Name, ext.CassandraQuery)
assert.Equal(span.Resource, "CREATE KEYSPACE trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
assert.Equal(span.Service, "ServiceName")
assert.Equal(span.GetMeta(ext.CassandraConsistencyLevel), "4")
assert.Equal(span.GetMeta(ext.CassandraPaginated), "false")
// Not added in case of an error
assert.Equal(span.GetMeta(ext.TargetHost), "")
assert.Equal(span.GetMeta(ext.TargetPort), "")
assert.Equal(span.GetMeta(ext.CassandraCluster), "")
assert.Equal(span.GetMeta(ext.CassandraKeyspace), "")
}
func TestChildWrapperSpan(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parentSpan := testTracer.NewChildSpanFromContext("parentSpan", ctx)
ctx = tracer.ContextWithSpan(ctx, parentSpan)
cluster := newCassandraCluster()
session, _ := cluster.CreateSession()
q := session.Query("SELECT * from trace.person")
tq := TraceQuery("TestServiceName", testTracer, q)
tq.WithContext(ctx).Exec()
parentSpan.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var childSpan, pSpan *tracer.Span
if spans[0].ParentID == spans[1].SpanID {
childSpan = spans[0]
pSpan = spans[1]
} else {
childSpan = spans[1]
pSpan = spans[0]
}
assert.Equal(pSpan.Name, "parentSpan")
assert.Equal(childSpan.ParentID, pSpan.SpanID)
assert.Equal(childSpan.Name, ext.CassandraQuery)
assert.Equal(childSpan.Resource, "SELECT * from trace.person")
assert.Equal(childSpan.GetMeta(ext.CassandraKeyspace), "trace")
assert.Equal(childSpan.GetMeta(ext.TargetPort), "59042")
assert.Equal(childSpan.GetMeta(ext.TargetHost), "127.0.0.1")
assert.Equal(childSpan.GetMeta(ext.CassandraCluster), "datacenter1")
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,11 @@
#!/bin/bash
# compiles test fixtures
set -e
protoc -I . fixtures.proto --go_out=plugins=grpc:.
# FIXME[matt] hacks to move the fixtures into the testing package
# and make it pass our lint rules. This is cheesy but very simple.
mv fixtures.pb.go fixtures_test.go
sed -i 's/_Fixture_Ping_Handler/fixturePingHandler/' fixtures_test.go
sed -i 's/_Fixture_serviceDesc/fixtureServiceDesc/' fixtures_test.go

View file

@ -0,0 +1,22 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.testgrpc";
option java_outer_classname = "TestGRPCProto";
package grpc;
service Fixture {
rpc Ping (FixtureRequest) returns (FixtureReply) {}
}
// The request message containing the user's name.
message FixtureRequest {
string name = 1;
}
// The response message containing the greetings
message FixtureReply {
string message = 1;
}

View file

@ -0,0 +1,164 @@
// Code generated by protoc-gen-go.
// source: fixtures.proto
// DO NOT EDIT!
/*
Package grpc is a generated protocol buffer package.
It is generated from these files:
fixtures.proto
It has these top-level messages:
FixtureRequest
FixtureReply
*/
package grpc
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the user's name.
type FixtureRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *FixtureRequest) Reset() { *m = FixtureRequest{} }
func (m *FixtureRequest) String() string { return proto.CompactTextString(m) }
func (*FixtureRequest) ProtoMessage() {}
func (*FixtureRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *FixtureRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
// The response message containing the greetings
type FixtureReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
func (m *FixtureReply) Reset() { *m = FixtureReply{} }
func (m *FixtureReply) String() string { return proto.CompactTextString(m) }
func (*FixtureReply) ProtoMessage() {}
func (*FixtureReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *FixtureReply) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterType((*FixtureRequest)(nil), "grpc.FixtureRequest")
proto.RegisterType((*FixtureReply)(nil), "grpc.FixtureReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Fixture service
type FixtureClient interface {
Ping(ctx context.Context, in *FixtureRequest, opts ...grpc.CallOption) (*FixtureReply, error)
}
type fixtureClient struct {
cc *grpc.ClientConn
}
func NewFixtureClient(cc *grpc.ClientConn) FixtureClient {
return &fixtureClient{cc}
}
func (c *fixtureClient) Ping(ctx context.Context, in *FixtureRequest, opts ...grpc.CallOption) (*FixtureReply, error) {
out := new(FixtureReply)
err := grpc.Invoke(ctx, "/grpc.Fixture/Ping", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Fixture service
type FixtureServer interface {
Ping(context.Context, *FixtureRequest) (*FixtureReply, error)
}
func RegisterFixtureServer(s *grpc.Server, srv FixtureServer) {
s.RegisterService(&fixtureServiceDesc, srv)
}
func fixturePingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FixtureRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(FixtureServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/grpc.Fixture/Ping",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FixtureServer).Ping(ctx, req.(*FixtureRequest))
}
return interceptor(ctx, in, info, handler)
}
var fixtureServiceDesc = grpc.ServiceDesc{
ServiceName: "grpc.Fixture",
HandlerType: (*FixtureServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Ping",
Handler: fixturePingHandler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "fixtures.proto",
}
func init() { proto.RegisterFile("fixtures.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 180 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0xcb, 0xac, 0x28,
0x29, 0x2d, 0x4a, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2c, 0x29, 0x4a, 0x4c,
0x4e, 0x4d, 0x2f, 0x2a, 0x48, 0x56, 0x52, 0xe1, 0xe2, 0x73, 0x83, 0x48, 0x06, 0xa5, 0x16, 0x96,
0xa6, 0x16, 0x97, 0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a,
0x70, 0x06, 0x81, 0xd9, 0x4a, 0x1a, 0x5c, 0x3c, 0x70, 0x55, 0x05, 0x39, 0x95, 0x42, 0x12, 0x5c,
0xec, 0xb9, 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0x30, 0x65, 0x30, 0xae, 0x91, 0x3b, 0x17, 0x3b, 0x54,
0xa5, 0x90, 0x0d, 0x17, 0x4b, 0x40, 0x66, 0x5e, 0xba, 0x90, 0xa4, 0x1e, 0xdc, 0x3a, 0x3d, 0x54,
0xbb, 0xa4, 0xc4, 0xb1, 0x49, 0x15, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0xe9, 0x70, 0x49, 0x66, 0xe6,
0xeb, 0x81, 0x65, 0x52, 0x2b, 0x12, 0x73, 0x0b, 0x72, 0x52, 0x8b, 0xf5, 0x4a, 0x52, 0x8b, 0x4b,
0x40, 0x22, 0x4e, 0xbc, 0x21, 0xa9, 0xc5, 0x25, 0xee, 0x41, 0x01, 0xce, 0x01, 0x20, 0xff, 0x04,
0x30, 0x26, 0xb1, 0x81, 0x3d, 0x66, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x68, 0x5d, 0x74, 0x4a,
0xea, 0x00, 0x00, 0x00,
}

View file

@ -0,0 +1,118 @@
package grpc
import (
"fmt"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
context "golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// pass trace ids with these headers
const (
traceIDKey = "x-datadog-trace-id"
parentIDKey = "x-datadog-parent-id"
)
// UnaryServerInterceptor will trace requests to the given grpc server.
func UnaryServerInterceptor(service string, t *tracer.Tracer) grpc.UnaryServerInterceptor {
t.SetServiceInfo(service, "grpc-server", ext.AppTypeRPC)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !t.Enabled() {
return handler(ctx, req)
}
span := serverSpan(t, ctx, info.FullMethod, service)
resp, err := handler(tracer.ContextWithSpan(ctx, span), req)
span.FinishWithErr(err)
return resp, err
}
}
// UnaryClientInterceptor will add tracing to a gprc client.
func UnaryClientInterceptor(service string, t *tracer.Tracer) grpc.UnaryClientInterceptor {
t.SetServiceInfo(service, "grpc-client", ext.AppTypeRPC)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var child *tracer.Span
span, ok := tracer.SpanFromContext(ctx)
// only trace the request if this is already part of a trace.
// does this make sense?
if ok && span.Tracer() != nil {
t := span.Tracer()
child = t.NewChildSpan("grpc.client", span)
child.SetMeta("grpc.method", method)
ctx = setIDs(child, ctx)
ctx = tracer.ContextWithSpan(ctx, child)
// FIXME[matt] add the host / port information here
// https://github.com/grpc/grpc-go/issues/951
}
err := invoker(ctx, method, req, reply, cc, opts...)
if child != nil {
child.SetMeta("grpc.code", grpc.Code(err).String())
child.FinishWithErr(err)
}
return err
}
}
func serverSpan(t *tracer.Tracer, ctx context.Context, method, service string) *tracer.Span {
span := t.NewRootSpan("grpc.server", service, method)
span.SetMeta("gprc.method", method)
span.Type = "go"
traceID, parentID := getIDs(ctx)
if traceID != 0 && parentID != 0 {
span.TraceID = traceID
span.ParentID = parentID
}
return span
}
// setIDs will set the trace ids on the context{
func setIDs(span *tracer.Span, ctx context.Context) context.Context {
if span == nil || span.TraceID == 0 {
return ctx
}
md := metadata.New(map[string]string{
traceIDKey: fmt.Sprint(span.TraceID),
parentIDKey: fmt.Sprint(span.ParentID),
})
if existing, ok := metadata.FromContext(ctx); ok {
md = metadata.Join(existing, md)
}
return metadata.NewContext(ctx, md)
}
// getIDs will return ids embededd an ahe context.
func getIDs(ctx context.Context) (traceID, parentID uint64) {
if md, ok := metadata.FromContext(ctx); ok {
if id := getID(md, traceIDKey); id > 0 {
traceID = id
}
if id := getID(md, parentIDKey); id > 0 {
parentID = id
}
}
return traceID, parentID
}
// getID parses an id from the metadata.
func getID(md metadata.MD, name string) uint64 {
for _, str := range md[name] {
id, err := strconv.Atoi(str)
if err == nil {
return uint64(id)
}
}
return 0
}

View file

@ -0,0 +1,294 @@
package grpc
import (
"fmt"
"net"
"net/http"
"testing"
"google.golang.org/grpc"
context "golang.org/x/net/context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/stretchr/testify/assert"
)
const (
debug = false
)
func TestClient(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, true)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
span := testTracer.NewRootSpan("a", "b", "c")
ctx := tracer.ContextWithSpan(context.Background(), span)
resp, err := client.Ping(ctx, &FixtureRequest{Name: "pass"})
assert.Nil(err)
span.Finish()
assert.Equal(resp.Message, "passed")
testTracer.ForceFlush()
traces := testTransport.Traces()
// A word here about what is going on: this is technically a
// distributed trace, while we're in this example in the Go world
// and within the same exec, client could know about server details.
// But this is not the general cases. So, as we only connect client
// and server through their span IDs, they can be flushed as independant
// traces. They could also be flushed at once, this is an implementation
// detail, what is important is that all of it is flushed, at some point.
if len(traces) == 0 {
assert.Fail("there should be at least one trace")
}
var spans []*tracer.Span
for _, trace := range traces {
for _, span := range trace {
spans = append(spans, span)
}
}
assert.Len(spans, 3)
var sspan, cspan, tspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "grpc.server":
sspan = s
case "grpc.client":
cspan = s
case "a":
tspan = s
}
}
assert.NotNil(sspan, "there should be a span with 'grpc.server' as Name")
assert.NotNil(cspan, "there should be a span with 'grpc.client' as Name")
assert.Equal(cspan.GetMeta("grpc.code"), "OK")
assert.NotNil(tspan, "there should be a span with 'a' as Name")
assert.Equal(cspan.TraceID, tspan.TraceID)
assert.Equal(sspan.TraceID, tspan.TraceID)
}
func TestDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
testTracer.SetEnabled(false)
rig, err := newRig(testTracer, true)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "disabled"})
assert.Nil(err)
assert.Equal(resp.Message, "disabled")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Nil(traces)
}
func TestChild(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, false)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "child"})
assert.Nil(err)
assert.Equal(resp.Message, "child")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var sspan, cspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "grpc.server":
sspan = s
case "child":
cspan = s
}
}
assert.NotNil(cspan, "there should be a span with 'child' as Name")
assert.Equal(cspan.Error, int32(0))
assert.Equal(cspan.Service, "grpc")
assert.Equal(cspan.Resource, "child")
assert.True(cspan.Duration > 0)
assert.NotNil(sspan, "there should be a span with 'grpc.server' as Name")
assert.Equal(sspan.Error, int32(0))
assert.Equal(sspan.Service, "grpc")
assert.Equal(sspan.Resource, "/grpc.Fixture/Ping")
assert.True(sspan.Duration > 0)
}
func TestPass(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, false)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "pass"})
assert.Nil(err)
assert.Equal(resp.Message, "passed")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Error, int32(0))
assert.Equal(s.Name, "grpc.server")
assert.Equal(s.Service, "grpc")
assert.Equal(s.Resource, "/grpc.Fixture/Ping")
assert.Equal(s.Type, "go")
assert.True(s.Duration > 0)
}
// fixtureServer a dummy implemenation of our grpc fixtureServer.
type fixtureServer struct{}
func newFixtureServer() *fixtureServer {
return &fixtureServer{}
}
func (s *fixtureServer) Ping(ctx context.Context, in *FixtureRequest) (*FixtureReply, error) {
switch {
case in.Name == "child":
span, ok := tracer.SpanFromContext(ctx)
if ok {
t := span.Tracer()
t.NewChildSpan("child", span).Finish()
}
return &FixtureReply{Message: "child"}, nil
case in.Name == "disabled":
_, ok := tracer.SpanFromContext(ctx)
if ok {
panic("should be disabled")
}
return &FixtureReply{Message: "disabled"}, nil
}
return &FixtureReply{Message: "passed"}, nil
}
// ensure it's a fixtureServer
var _ FixtureServer = &fixtureServer{}
// rig contains all of the servers and connections we'd need for a
// grpc integration test
type rig struct {
server *grpc.Server
listener net.Listener
conn *grpc.ClientConn
client FixtureClient
}
func (r *rig) Close() {
r.server.Stop()
r.conn.Close()
r.listener.Close()
}
func newRig(t *tracer.Tracer, traceClient bool) (*rig, error) {
server := grpc.NewServer(grpc.UnaryInterceptor(UnaryServerInterceptor("grpc", t)))
RegisterFixtureServer(server, newFixtureServer())
li, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return nil, err
}
// start our test fixtureServer.
go server.Serve(li)
opts := []grpc.DialOption{
grpc.WithInsecure(),
}
if traceClient {
opts = append(opts, grpc.WithUnaryInterceptor(UnaryClientInterceptor("grpc", t)))
}
conn, err := grpc.Dial(li.Addr().String(), opts...)
if err != nil {
return nil, fmt.Errorf("error dialing: %s", err)
}
r := &rig{
listener: li,
server: server,
conn: conn,
client: NewFixtureClient(conn),
}
return r, err
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,11 @@
#!/bin/bash
# compiles test fixtures
set -e
protoc -I . fixtures.proto --go_out=plugins=grpc:.
# FIXME[matt] hacks to move the fixtures into the testing package
# and make it pass our lint rules. This is cheesy but very simple.
mv fixtures.pb.go fixtures_test.go
sed -i 's/_Fixture_Ping_Handler/fixturePingHandler/' fixtures_test.go
sed -i 's/_Fixture_serviceDesc/fixtureServiceDesc/' fixtures_test.go

View file

@ -0,0 +1,22 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.testgrpc";
option java_outer_classname = "TestGRPCProto";
package grpc;
service Fixture {
rpc Ping (FixtureRequest) returns (FixtureReply) {}
}
// The request message containing the user's name.
message FixtureRequest {
string name = 1;
}
// The response message containing the greetings
message FixtureReply {
string message = 1;
}

View file

@ -0,0 +1,164 @@
// Code generated by protoc-gen-go.
// source: fixtures.proto
// DO NOT EDIT!
/*
Package grpc is a generated protocol buffer package.
It is generated from these files:
fixtures.proto
It has these top-level messages:
FixtureRequest
FixtureReply
*/
package grpc
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the user's name.
type FixtureRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *FixtureRequest) Reset() { *m = FixtureRequest{} }
func (m *FixtureRequest) String() string { return proto.CompactTextString(m) }
func (*FixtureRequest) ProtoMessage() {}
func (*FixtureRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *FixtureRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
// The response message containing the greetings
type FixtureReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
func (m *FixtureReply) Reset() { *m = FixtureReply{} }
func (m *FixtureReply) String() string { return proto.CompactTextString(m) }
func (*FixtureReply) ProtoMessage() {}
func (*FixtureReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *FixtureReply) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterType((*FixtureRequest)(nil), "grpc.FixtureRequest")
proto.RegisterType((*FixtureReply)(nil), "grpc.FixtureReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Fixture service
type FixtureClient interface {
Ping(ctx context.Context, in *FixtureRequest, opts ...grpc.CallOption) (*FixtureReply, error)
}
type fixtureClient struct {
cc *grpc.ClientConn
}
func NewFixtureClient(cc *grpc.ClientConn) FixtureClient {
return &fixtureClient{cc}
}
func (c *fixtureClient) Ping(ctx context.Context, in *FixtureRequest, opts ...grpc.CallOption) (*FixtureReply, error) {
out := new(FixtureReply)
err := grpc.Invoke(ctx, "/grpc.Fixture/Ping", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Fixture service
type FixtureServer interface {
Ping(context.Context, *FixtureRequest) (*FixtureReply, error)
}
func RegisterFixtureServer(s *grpc.Server, srv FixtureServer) {
s.RegisterService(&fixtureServiceDesc, srv)
}
func fixturePingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FixtureRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(FixtureServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/grpc.Fixture/Ping",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FixtureServer).Ping(ctx, req.(*FixtureRequest))
}
return interceptor(ctx, in, info, handler)
}
var fixtureServiceDesc = grpc.ServiceDesc{
ServiceName: "grpc.Fixture",
HandlerType: (*FixtureServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Ping",
Handler: fixturePingHandler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "fixtures.proto",
}
func init() { proto.RegisterFile("fixtures.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 180 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0xcb, 0xac, 0x28,
0x29, 0x2d, 0x4a, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2c, 0x29, 0x4a, 0x4c,
0x4e, 0x4d, 0x2f, 0x2a, 0x48, 0x56, 0x52, 0xe1, 0xe2, 0x73, 0x83, 0x48, 0x06, 0xa5, 0x16, 0x96,
0xa6, 0x16, 0x97, 0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a,
0x70, 0x06, 0x81, 0xd9, 0x4a, 0x1a, 0x5c, 0x3c, 0x70, 0x55, 0x05, 0x39, 0x95, 0x42, 0x12, 0x5c,
0xec, 0xb9, 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0x30, 0x65, 0x30, 0xae, 0x91, 0x3b, 0x17, 0x3b, 0x54,
0xa5, 0x90, 0x0d, 0x17, 0x4b, 0x40, 0x66, 0x5e, 0xba, 0x90, 0xa4, 0x1e, 0xdc, 0x3a, 0x3d, 0x54,
0xbb, 0xa4, 0xc4, 0xb1, 0x49, 0x15, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0xe9, 0x70, 0x49, 0x66, 0xe6,
0xeb, 0x81, 0x65, 0x52, 0x2b, 0x12, 0x73, 0x0b, 0x72, 0x52, 0x8b, 0xf5, 0x4a, 0x52, 0x8b, 0x4b,
0x40, 0x22, 0x4e, 0xbc, 0x21, 0xa9, 0xc5, 0x25, 0xee, 0x41, 0x01, 0xce, 0x01, 0x20, 0xff, 0x04,
0x30, 0x26, 0xb1, 0x81, 0x3d, 0x66, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x68, 0x5d, 0x74, 0x4a,
0xea, 0x00, 0x00, 0x00,
}

View file

@ -0,0 +1,118 @@
package grpc
import (
"fmt"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
context "golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// pass trace ids with these headers
const (
traceIDKey = "x-datadog-trace-id"
parentIDKey = "x-datadog-parent-id"
)
// UnaryServerInterceptor will trace requests to the given grpc server.
func UnaryServerInterceptor(service string, t *tracer.Tracer) grpc.UnaryServerInterceptor {
t.SetServiceInfo(service, "grpc-server", ext.AppTypeRPC)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !t.Enabled() {
return handler(ctx, req)
}
span := serverSpan(t, ctx, info.FullMethod, service)
resp, err := handler(tracer.ContextWithSpan(ctx, span), req)
span.FinishWithErr(err)
return resp, err
}
}
// UnaryClientInterceptor will add tracing to a gprc client.
func UnaryClientInterceptor(service string, t *tracer.Tracer) grpc.UnaryClientInterceptor {
t.SetServiceInfo(service, "grpc-client", ext.AppTypeRPC)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var child *tracer.Span
span, ok := tracer.SpanFromContext(ctx)
// only trace the request if this is already part of a trace.
// does this make sense?
if ok && span.Tracer() != nil {
t := span.Tracer()
child = t.NewChildSpan("grpc.client", span)
child.SetMeta("grpc.method", method)
ctx = setIDs(child, ctx)
ctx = tracer.ContextWithSpan(ctx, child)
// FIXME[matt] add the host / port information here
// https://github.com/grpc/grpc-go/issues/951
}
err := invoker(ctx, method, req, reply, cc, opts...)
if child != nil {
child.SetMeta("grpc.code", grpc.Code(err).String())
child.FinishWithErr(err)
}
return err
}
}
func serverSpan(t *tracer.Tracer, ctx context.Context, method, service string) *tracer.Span {
span := t.NewRootSpan("grpc.server", service, method)
span.SetMeta("gprc.method", method)
span.Type = "go"
traceID, parentID := getIDs(ctx)
if traceID != 0 && parentID != 0 {
span.TraceID = traceID
span.ParentID = parentID
}
return span
}
// setIDs will set the trace ids on the context{
func setIDs(span *tracer.Span, ctx context.Context) context.Context {
if span == nil || span.TraceID == 0 {
return ctx
}
md := metadata.New(map[string]string{
traceIDKey: fmt.Sprint(span.TraceID),
parentIDKey: fmt.Sprint(span.ParentID),
})
if existing, ok := metadata.FromIncomingContext(ctx); ok {
md = metadata.Join(existing, md)
}
return metadata.NewOutgoingContext(ctx, md)
}
// getIDs will return ids embededd an ahe context.
func getIDs(ctx context.Context) (traceID, parentID uint64) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if id := getID(md, traceIDKey); id > 0 {
traceID = id
}
if id := getID(md, parentIDKey); id > 0 {
parentID = id
}
}
return traceID, parentID
}
// getID parses an id from the metadata.
func getID(md metadata.MD, name string) uint64 {
for _, str := range md[name] {
id, err := strconv.Atoi(str)
if err == nil {
return uint64(id)
}
}
return 0
}

View file

@ -0,0 +1,294 @@
package grpc
import (
"fmt"
"net"
"net/http"
"testing"
"google.golang.org/grpc"
context "golang.org/x/net/context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/stretchr/testify/assert"
)
const (
debug = false
)
func TestClient(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, true)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
span := testTracer.NewRootSpan("a", "b", "c")
ctx := tracer.ContextWithSpan(context.Background(), span)
resp, err := client.Ping(ctx, &FixtureRequest{Name: "pass"})
assert.Nil(err)
span.Finish()
assert.Equal(resp.Message, "passed")
testTracer.ForceFlush()
traces := testTransport.Traces()
// A word here about what is going on: this is technically a
// distributed trace, while we're in this example in the Go world
// and within the same exec, client could know about server details.
// But this is not the general cases. So, as we only connect client
// and server through their span IDs, they can be flushed as independant
// traces. They could also be flushed at once, this is an implementation
// detail, what is important is that all of it is flushed, at some point.
if len(traces) == 0 {
assert.Fail("there should be at least one trace")
}
var spans []*tracer.Span
for _, trace := range traces {
for _, span := range trace {
spans = append(spans, span)
}
}
assert.Len(spans, 3)
var sspan, cspan, tspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "grpc.server":
sspan = s
case "grpc.client":
cspan = s
case "a":
tspan = s
}
}
assert.NotNil(sspan, "there should be a span with 'grpc.server' as Name")
assert.NotNil(cspan, "there should be a span with 'grpc.client' as Name")
assert.Equal(cspan.GetMeta("grpc.code"), "OK")
assert.NotNil(tspan, "there should be a span with 'a' as Name")
assert.Equal(cspan.TraceID, tspan.TraceID)
assert.Equal(sspan.TraceID, tspan.TraceID)
}
func TestDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
testTracer.SetEnabled(false)
rig, err := newRig(testTracer, true)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "disabled"})
assert.Nil(err)
assert.Equal(resp.Message, "disabled")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Nil(traces)
}
func TestChild(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, false)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "child"})
assert.Nil(err)
assert.Equal(resp.Message, "child")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var sspan, cspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "grpc.server":
sspan = s
case "child":
cspan = s
}
}
assert.NotNil(cspan, "there should be a span with 'child' as Name")
assert.Equal(cspan.Error, int32(0))
assert.Equal(cspan.Service, "grpc")
assert.Equal(cspan.Resource, "child")
assert.True(cspan.Duration > 0)
assert.NotNil(sspan, "there should be a span with 'grpc.server' as Name")
assert.Equal(sspan.Error, int32(0))
assert.Equal(sspan.Service, "grpc")
assert.Equal(sspan.Resource, "/grpc.Fixture/Ping")
assert.True(sspan.Duration > 0)
}
func TestPass(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
rig, err := newRig(testTracer, false)
if err != nil {
t.Fatalf("error setting up rig: %s", err)
}
defer rig.Close()
client := rig.client
resp, err := client.Ping(context.Background(), &FixtureRequest{Name: "pass"})
assert.Nil(err)
assert.Equal(resp.Message, "passed")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Error, int32(0))
assert.Equal(s.Name, "grpc.server")
assert.Equal(s.Service, "grpc")
assert.Equal(s.Resource, "/grpc.Fixture/Ping")
assert.Equal(s.Type, "go")
assert.True(s.Duration > 0)
}
// fixtureServer a dummy implemenation of our grpc fixtureServer.
type fixtureServer struct{}
func newFixtureServer() *fixtureServer {
return &fixtureServer{}
}
func (s *fixtureServer) Ping(ctx context.Context, in *FixtureRequest) (*FixtureReply, error) {
switch {
case in.Name == "child":
span, ok := tracer.SpanFromContext(ctx)
if ok {
t := span.Tracer()
t.NewChildSpan("child", span).Finish()
}
return &FixtureReply{Message: "child"}, nil
case in.Name == "disabled":
_, ok := tracer.SpanFromContext(ctx)
if ok {
panic("should be disabled")
}
return &FixtureReply{Message: "disabled"}, nil
}
return &FixtureReply{Message: "passed"}, nil
}
// ensure it's a fixtureServer
var _ FixtureServer = &fixtureServer{}
// rig contains all of the servers and connections we'd need for a
// grpc integration test
type rig struct {
server *grpc.Server
listener net.Listener
conn *grpc.ClientConn
client FixtureClient
}
func (r *rig) Close() {
r.server.Stop()
r.conn.Close()
r.listener.Close()
}
func newRig(t *tracer.Tracer, traceClient bool) (*rig, error) {
server := grpc.NewServer(grpc.UnaryInterceptor(UnaryServerInterceptor("grpc", t)))
RegisterFixtureServer(server, newFixtureServer())
li, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return nil, err
}
// start our test fixtureServer.
go server.Serve(li)
opts := []grpc.DialOption{
grpc.WithInsecure(),
}
if traceClient {
opts = append(opts, grpc.WithUnaryInterceptor(UnaryClientInterceptor("grpc", t)))
}
conn, err := grpc.Dial(li.Addr().String(), opts...)
if err != nil {
return nil, fmt.Errorf("error dialing: %s", err)
}
r := &rig{
listener: li,
server: server,
conn: conn,
client: NewFixtureClient(conn),
}
return r, err
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,31 @@
package mux_test
import (
"fmt"
"net/http"
muxtrace "github.com/DataDog/dd-trace-go/contrib/gorilla/mux"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gorilla/mux"
)
// handler is a simple handlerFunc that logs some data from the span
// that is injected into the requests' context.
func handler(w http.ResponseWriter, r *http.Request) {
span := tracer.SpanFromContextDefault(r.Context())
fmt.Printf("tracing service:%s resource:%s", span.Service, span.Resource)
w.Write([]byte("hello world"))
}
func Example() {
router := mux.NewRouter()
muxTracer := muxtrace.NewMuxTracer("my-web-app", tracer.DefaultTracer)
// Add traced routes directly.
muxTracer.HandleFunc(router, "/users", handler)
// and subroutes as well.
subrouter := router.PathPrefix("/user").Subrouter()
muxTracer.HandleFunc(subrouter, "/view", handler)
muxTracer.HandleFunc(subrouter, "/create", handler)
}

View file

@ -0,0 +1,127 @@
// Package mux provides tracing functions for the Gorilla Mux framework.
package mux
import (
"net/http"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gorilla/mux"
)
// MuxTracer is used to trace requests in a mux server.
type MuxTracer struct {
tracer *tracer.Tracer
service string
}
// NewMuxTracer creates a MuxTracer for the given service and tracer.
func NewMuxTracer(service string, t *tracer.Tracer) *MuxTracer {
t.SetServiceInfo(service, "gorilla", ext.AppTypeWeb)
return &MuxTracer{
tracer: t,
service: service,
}
}
// TraceHandleFunc will return a HandlerFunc that will wrap tracing around the
// given handler func.
func (m *MuxTracer) TraceHandleFunc(handler http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, req *http.Request) {
// bail our if tracing isn't enabled.
if !m.tracer.Enabled() {
handler(writer, req)
return
}
// trace the request
tracedRequest, span := m.trace(req)
defer span.Finish()
// trace the response
tracedWriter := newTracedResponseWriter(span, writer)
// run the request
handler(tracedWriter, tracedRequest)
}
}
// HandleFunc will add a traced version of the given handler to the router.
func (m *MuxTracer) HandleFunc(router *mux.Router, pattern string, handler http.HandlerFunc) *mux.Route {
return router.HandleFunc(pattern, m.TraceHandleFunc(handler))
}
// span will create a span for the given request.
func (m *MuxTracer) trace(req *http.Request) (*http.Request, *tracer.Span) {
route := mux.CurrentRoute(req)
path, err := route.GetPathTemplate()
if err != nil {
// when route doesn't define a path
path = "unknown"
}
resource := req.Method + " " + path
span := m.tracer.NewRootSpan("mux.request", m.service, resource)
span.Type = ext.HTTPType
span.SetMeta(ext.HTTPMethod, req.Method)
span.SetMeta(ext.HTTPURL, path)
// patch the span onto the request context.
treq := SetRequestSpan(req, span)
return treq, span
}
// tracedResponseWriter is a small wrapper around an http response writer that will
// intercept and store the status of a request.
type tracedResponseWriter struct {
span *tracer.Span
w http.ResponseWriter
status int
}
func newTracedResponseWriter(span *tracer.Span, w http.ResponseWriter) *tracedResponseWriter {
return &tracedResponseWriter{
span: span,
w: w}
}
func (t *tracedResponseWriter) Header() http.Header {
return t.w.Header()
}
func (t *tracedResponseWriter) Write(b []byte) (int, error) {
if t.status == 0 {
t.WriteHeader(http.StatusOK)
}
return t.w.Write(b)
}
func (t *tracedResponseWriter) WriteHeader(status int) {
t.w.WriteHeader(status)
t.status = status
t.span.SetMeta(ext.HTTPCode, strconv.Itoa(status))
if status >= 500 && status < 600 {
t.span.Error = 1
}
}
// SetRequestSpan sets the span on the request's context.
func SetRequestSpan(r *http.Request, span *tracer.Span) *http.Request {
if r == nil || span == nil {
return r
}
ctx := tracer.ContextWithSpan(r.Context(), span)
return r.WithContext(ctx)
}
// GetRequestSpan will return the span associated with the given request. It
// will return nil/false if it doesn't exist.
func GetRequestSpan(r *http.Request) (*tracer.Span, bool) {
span, ok := tracer.SpanFromContext(r.Context())
return span, ok
}

View file

@ -0,0 +1,206 @@
package mux
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestMuxTracerDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport, muxTracer := getTestTracer("disabled-service")
router := mux.NewRouter()
muxTracer.HandleFunc(router, "/disabled", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("disabled!"))
assert.Nil(err)
// Ensure we have no tracing context.
span, ok := tracer.SpanFromContext(r.Context())
assert.Nil(span)
assert.False(ok)
})
testTracer.SetEnabled(false) // the key line in this test.
// make the request
req := httptest.NewRequest("GET", "/disabled", nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "disabled!")
// assert nothing was traced.
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 0)
}
func TestMuxTracerSubrequest(t *testing.T) {
assert := assert.New(t)
// Send and verify a 200 request
for _, url := range []string{"/sub/child1", "/sub/child2"} {
tracer, transport, router := setup(t)
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "200!")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(0))
}
}
func TestMuxTracer200(t *testing.T) {
assert := assert.New(t)
// setup
tracer, transport, router := setup(t)
// Send and verify a 200 request
url := "/200"
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "200!")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(0))
}
func TestMuxTracer500(t *testing.T) {
assert := assert.New(t)
// setup
tracer, transport, router := setup(t)
// SEnd and verify a 200 request
url := "/500"
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 500)
assert.Equal(writer.Body.String(), "500!\n")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "500")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(1))
}
// test handlers
func handler200(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("200!"))
assert.Nil(err)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal(span.Service, "my-service")
assert.Equal(span.Duration, int64(0))
}
}
func handler500(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500!", http.StatusInternalServerError)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal(span.Service, "my-service")
assert.Equal(span.Duration, int64(0))
}
}
func setup(t *testing.T) (*tracer.Tracer, *dummyTransport, *mux.Router) {
tracer, transport, mt := getTestTracer("my-service")
r := mux.NewRouter()
h200 := handler200(t)
h500 := handler500(t)
// Ensure we can use HandleFunc and it returns a route
mt.HandleFunc(r, "/200", h200).Methods("Get")
// And we can allso handle a bare func
r.HandleFunc("/500", mt.TraceHandleFunc(h500))
// do a subrouter (one in each way)
sub := r.PathPrefix("/sub").Subrouter()
sub.HandleFunc("/child1", mt.TraceHandleFunc(h200))
mt.HandleFunc(sub, "/child2", h200)
return tracer, transport, r
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer(service string) (*tracer.Tracer, *dummyTransport, *MuxTracer) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
muxTracer := NewMuxTracer(service, tracer)
return tracer, transport, muxTracer
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,23 @@
package sqlx_test
import (
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
sqlxtrace "github.com/DataDog/dd-trace-go/contrib/jmoiron/sqlx"
)
// The API to trace sqlx calls is the same as sqltraced.
// See https://godoc.org/github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced for more information on how to use it.
func Example() {
// OpenTraced will first register a traced version of the driver and then will return the sqlx.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, _ := sqlxtrace.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
// All calls through sqlx API will then be traced.
query, args, _ := sqlx.In("SELECT * FROM users WHERE level IN (?);", []int{4, 6, 7})
query = db.Rebind(query)
rows, _ := db.Query(query, args...)
defer rows.Close()
}

View file

@ -0,0 +1,41 @@
package sqlx
import (
"log"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/sqltest"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/go-sql-driver/mysql"
)
func TestMySQL(t *testing.T) {
trc, transport := tracertest.GetTestTracer()
dbx, err := OpenTraced(&mysql.MySQLDriver{}, "test:test@tcp(127.0.0.1:53306)/test", "mysql-test", trc)
if err != nil {
log.Fatal(err)
}
defer dbx.Close()
testDB := &sqltest.DB{
DB: dbx.DB,
Tracer: trc,
Transport: transport,
DriverName: "mysql",
}
expectedSpan := &tracer.Span{
Name: "mysql.query",
Service: "mysql-test",
Type: "sql",
}
expectedSpan.Meta = map[string]string{
"db.user": "test",
"out.host": "127.0.0.1",
"out.port": "53306",
"db.name": "test",
}
sqltest.AllSQLTests(t, testDB, expectedSpan)
}

View file

@ -0,0 +1,41 @@
package sqlx
import (
"log"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/sqltest"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/lib/pq"
)
func TestPostgres(t *testing.T) {
trc, transport := tracertest.GetTestTracer()
dbx, err := OpenTraced(&pq.Driver{}, "postgres://postgres:postgres@127.0.0.1:55432/postgres?sslmode=disable", "postgres-test", trc)
if err != nil {
log.Fatal(err)
}
defer dbx.Close()
testDB := &sqltest.DB{
DB: dbx.DB,
Tracer: trc,
Transport: transport,
DriverName: "postgres",
}
expectedSpan := &tracer.Span{
Name: "postgres.query",
Service: "postgres-test",
Type: "sql",
}
expectedSpan.Meta = map[string]string{
"db.user": "postgres",
"out.host": "127.0.0.1",
"out.port": "55432",
"db.name": "postgres",
}
sqltest.AllSQLTests(t, testDB, expectedSpan)
}

View file

@ -0,0 +1,35 @@
// Package sqlxtraced provides a traced version of the "jmoiron/sqlx" package
// For more information about the API, see https://godoc.org/github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced.
package sqlx
import (
"database/sql/driver"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/sqlutils"
"github.com/jmoiron/sqlx"
)
// OpenTraced will first register the traced version of the `driver` if not yet registered and will then open a connection with it.
// This is usually the only function to use when there is no need for the granularity offered by Register and Open.
// The last argument is optional and allows you to pass a custom tracer.
func OpenTraced(driver driver.Driver, dataSourceName, service string, trcv ...*tracer.Tracer) (*sqlx.DB, error) {
driverName := sqlutils.GetDriverName(driver)
Register(driverName, driver, trcv...)
return Open(driverName, dataSourceName, service)
}
// Register registers a traced version of `driver`.
func Register(driverName string, driver driver.Driver, trcv ...*tracer.Tracer) {
sqltraced.Register(driverName, driver, trcv...)
}
// Open returns a traced version of *sqlx.DB.
func Open(driverName, dataSourceName, service string) (*sqlx.DB, error) {
db, err := sqltraced.Open(driverName, dataSourceName, service)
if err != nil {
return nil, err
}
return sqlx.NewDb(db, driverName), err
}

View file

@ -0,0 +1,17 @@
package http_test
import (
"net/http"
httptrace "github.com/DataDog/dd-trace-go/contrib/net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!\n"))
}
func Example() {
mux := httptrace.NewServeMux("web-service", nil)
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", mux)
}

View file

@ -0,0 +1,93 @@
package http
import (
"net/http"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
)
// ServeMux is an HTTP request multiplexer that traces all the incoming requests.
type ServeMux struct {
*http.ServeMux
*tracer.Tracer
service string
}
// NewServeMux allocates and returns a new ServeMux.
func NewServeMux(service string, t *tracer.Tracer) *ServeMux {
if t == nil {
t = tracer.DefaultTracer
}
t.SetServiceInfo(service, "net/http", ext.AppTypeWeb)
return &ServeMux{http.NewServeMux(), t, service}
}
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
// We only needed to rewrite this method to be able to trace the multiplexer.
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// bail out if tracing isn't enabled
if !mux.Tracer.Enabled() {
mux.ServeMux.ServeHTTP(w, r)
return
}
// get the route associated to this request
_, route := mux.Handler(r)
// create a new span
resource := r.Method + " " + route
span := mux.Tracer.NewRootSpan("http.request", mux.service, resource)
defer span.Finish()
span.Type = ext.HTTPType
span.SetMeta(ext.HTTPMethod, r.Method)
span.SetMeta(ext.HTTPURL, r.URL.Path)
// pass the span through the request context
ctx := span.Context(r.Context())
traceRequest := r.WithContext(ctx)
// trace the response to get the status code
traceWriter := NewResponseWriter(w, span)
// serve the request to the underlying multiplexer
mux.ServeMux.ServeHTTP(traceWriter, traceRequest)
}
// ResponseWriter is a small wrapper around an http response writer that will
// intercept and store the status of a request.
// It implements the ResponseWriter interface.
type ResponseWriter struct {
http.ResponseWriter
span *tracer.Span
status int
}
// New ResponseWriter allocateds and returns a new ResponseWriter.
func NewResponseWriter(w http.ResponseWriter, span *tracer.Span) *ResponseWriter {
return &ResponseWriter{w, span, 0}
}
// Write writes the data to the connection as part of an HTTP reply.
// We explicitely call WriteHeader with the 200 status code
// in order to get it reported into the span.
func (w *ResponseWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
// WriteHeader sends an HTTP response header with status code.
// It also sets the status code to the span.
func (w *ResponseWriter) WriteHeader(status int) {
w.ResponseWriter.WriteHeader(status)
w.status = status
w.span.SetMeta(ext.HTTPCode, strconv.Itoa(status))
if status >= 500 && status < 600 {
w.span.Error = 1
}
}

View file

@ -0,0 +1,134 @@
package http
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/stretchr/testify/assert"
)
func TestHttpTracerDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := tracertest.GetTestTracer()
testTracer.SetEnabled(false)
mux := NewServeMux("my-service", testTracer)
mux.HandleFunc("/disabled", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("disabled!"))
assert.Nil(err)
// Ensure we have no tracing context
span, ok := tracer.SpanFromContext(r.Context())
assert.Nil(span)
assert.False(ok)
})
// Make the request
r := httptest.NewRequest("GET", "/disabled", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, r)
assert.Equal(200, w.Code)
assert.Equal("disabled!", w.Body.String())
// Assert nothing was traced
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Equal(0, len(traces))
}
func TestHttpTracer200(t *testing.T) {
assert := assert.New(t)
tracer, transport, router := setup(t)
// Send and verify a 200 request
url := "/200"
r := httptest.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
assert.Equal(200, w.Code)
assert.Equal("200!\n", w.Body.String())
// Ensure the request is properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Equal(1, len(traces))
spans := traces[0]
assert.Equal(1, len(spans))
s := spans[0]
assert.Equal("http.request", s.Name)
assert.Equal("my-service", s.Service)
assert.Equal("GET "+url, s.Resource)
assert.Equal("200", s.GetMeta("http.status_code"))
assert.Equal("GET", s.GetMeta("http.method"))
assert.Equal(url, s.GetMeta("http.url"))
assert.Equal(int32(0), s.Error)
}
func TestHttpTracer500(t *testing.T) {
assert := assert.New(t)
tracer, transport, router := setup(t)
// Send and verify a 500 request
url := "/500"
r := httptest.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
assert.Equal(500, w.Code)
assert.Equal("500!\n", w.Body.String())
// Ensure the request is properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Equal(1, len(traces))
spans := traces[0]
assert.Equal(1, len(spans))
s := spans[0]
assert.Equal("http.request", s.Name)
assert.Equal("my-service", s.Service)
assert.Equal("GET "+url, s.Resource)
assert.Equal("500", s.GetMeta("http.status_code"))
assert.Equal("GET", s.GetMeta("http.method"))
assert.Equal(url, s.GetMeta("http.url"))
assert.Equal(int32(1), s.Error)
}
func setup(t *testing.T) (*tracer.Tracer, *tracertest.DummyTransport, http.Handler) {
h200 := handler200(t)
h500 := handler500(t)
tracer, transport := tracertest.GetTestTracer()
mux := NewServeMux("my-service", tracer)
mux.HandleFunc("/200", h200)
mux.HandleFunc("/500", h500)
return tracer, transport, mux
}
func handler200(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("200!\n"))
assert.Nil(err)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal("my-service", span.Service)
assert.Equal(int64(0), span.Duration)
}
}
func handler500(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500!", http.StatusInternalServerError)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal("my-service", span.Service)
assert.Equal(int64(0), span.Duration)
}
}

View file

@ -0,0 +1,86 @@
// Package elastictrace provides tracing for the Elastic Elasticsearch client.
// Supports v3 (gopkg.in/olivere/elastic.v3), v5 (gopkg.in/olivere/elastic.v5)
// but with v3 you must use `DoC` on all requests to capture the request context.
package elastictrace
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
)
// MaxContentLength is the maximum content length for which we'll read and capture
// the contents of the request body. Anything larger will still be traced but the
// body will not be captured as trace metadata.
const MaxContentLength = 500 * 1024
// TracedTransport is a traced HTTP transport that captures Elasticsearch spans.
type TracedTransport struct {
service string
tracer *tracer.Tracer
*http.Transport
}
// RoundTrip satisfies the RoundTripper interface, wraps the sub Transport and
// captures a span of the Elasticsearch request.
func (t *TracedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
span := t.tracer.NewChildSpanFromContext("elasticsearch.query", req.Context())
span.Service = t.service
span.Type = ext.AppTypeDB
defer span.Finish()
span.SetMeta("elasticsearch.method", req.Method)
span.SetMeta("elasticsearch.url", req.URL.Path)
span.SetMeta("elasticsearch.params", req.URL.Query().Encode())
contentLength, _ := strconv.Atoi(req.Header.Get("Content-Length"))
if req.Body != nil && contentLength < MaxContentLength {
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
span.SetMeta("elasticsearch.body", string(buf))
req.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
}
// Run the request using the standard transport.
res, err := t.Transport.RoundTrip(req)
if res != nil {
span.SetMeta(ext.HTTPCode, strconv.Itoa(res.StatusCode))
}
if err != nil {
span.SetError(err)
} else if res.StatusCode < 200 || res.StatusCode > 299 {
buf, err := ioutil.ReadAll(res.Body)
if err != nil {
// Status text is best we can do if if we can't read the body.
span.SetError(errors.New(http.StatusText(res.StatusCode)))
} else {
span.SetError(errors.New(string(buf)))
}
res.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
}
Quantize(span)
return res, err
}
// NewTracedHTTPClient returns a new TracedTransport that traces HTTP requests.
func NewTracedHTTPClient(service string, tracer *tracer.Tracer) *http.Client {
return &http.Client{
Transport: &TracedTransport{service, tracer, &http.Transport{}},
}
}
// NewTracedHTTPClientWithTransport returns a new TracedTransport that traces HTTP requests
// and takes in a Transport to use something other than the default.
func NewTracedHTTPClientWithTransport(service string, tracer *tracer.Tracer, transport *http.Transport) *http.Client {
return &http.Client{
Transport: &TracedTransport{service, tracer, transport},
}
}

View file

@ -0,0 +1,193 @@
package elastictrace
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/stretchr/testify/assert"
elasticv3 "gopkg.in/olivere/elastic.v3"
elasticv5 "gopkg.in/olivere/elastic.v5"
"testing"
)
const (
debug = false
)
func TestClientV5(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv5.NewClient(
elasticv5.SetURL("http://127.0.0.1:59200"),
elasticv5.SetHttpClient(tc),
elasticv5.SetSniff(false),
elasticv5.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.TODO())
assert.NoError(err)
checkPUTTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("twitter").Type("tweet").
Id("1").Do(context.TODO())
assert.NoError(err)
checkGETTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("not-real-index").
Id("1").Do(context.TODO())
assert.Error(err)
checkErrTrace(assert, testTracer, testTransport)
}
func TestClientV3(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv3.NewClient(
elasticv3.SetURL("http://127.0.0.1:59201"),
elasticv3.SetHttpClient(tc),
elasticv3.SetSniff(false),
elasticv3.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.TODO())
assert.NoError(err)
checkPUTTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("twitter").Type("tweet").
Id("1").DoC(context.TODO())
assert.NoError(err)
checkGETTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("not-real-index").
Id("1").DoC(context.TODO())
assert.Error(err)
checkErrTrace(assert, testTracer, testTransport)
}
func TestClientV3Failure(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv3.NewClient(
// not existing service, it must fail
elasticv3.SetURL("http://127.0.0.1:29201"),
elasticv3.SetHttpClient(tc),
elasticv3.SetSniff(false),
elasticv3.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.TODO())
assert.Error(err)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*net.OpError", spans[0].GetMeta("error.type"))
}
func TestClientV5Failure(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv5.NewClient(
// not existing service, it must fail
elasticv5.SetURL("http://127.0.0.1:29200"),
elasticv5.SetHttpClient(tc),
elasticv5.SetSniff(false),
elasticv5.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.TODO())
assert.Error(err)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*net.OpError", spans[0].GetMeta("error.type"))
}
func checkPUTTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
}
func checkGETTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("GET /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("GET", spans[0].GetMeta("elasticsearch.method"))
}
func checkErrTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("GET /not-real-index/_all/?", spans[0].Resource)
assert.Equal("/not-real-index/_all/1", spans[0].GetMeta("elasticsearch.url"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*errors.errorString", spans[0].GetMeta("error.type"))
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *tracertest.DummyTransport) {
transport := &tracertest.DummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}

View file

@ -0,0 +1,57 @@
package elastictrace_test
import (
"context"
elastictrace "github.com/DataDog/dd-trace-go/contrib/olivere/elastic"
"github.com/DataDog/dd-trace-go/tracer"
elasticv3 "gopkg.in/olivere/elastic.v3"
elasticv5 "gopkg.in/olivere/elastic.v5"
)
// To start tracing elastic.v5 requests, create a new TracedHTTPClient that you will
// use when initializing the elastic.Client.
func Example_v5() {
tc := elastictrace.NewTracedHTTPClient("my-elasticsearch-service", tracer.DefaultTracer)
client, _ := elasticv5.NewClient(
elasticv5.SetURL("http://127.0.0.1:9200"),
elasticv5.SetHttpClient(tc),
)
// Spans are emitted for all
client.Index().
Index("twitter").Type("tweet").Index("1").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.Background())
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/tweet/1")
ctx := root.Context(context.Background())
client.Get().
Index("twitter").Type("tweet").Index("1").
Do(ctx)
root.Finish()
}
// To trace elastic.v3 you create a TracedHTTPClient in the same way but all requests must use
// the DoC() call to pass the request context.
func Example_v3() {
tc := elastictrace.NewTracedHTTPClient("my-elasticsearch-service", tracer.DefaultTracer)
client, _ := elasticv3.NewClient(
elasticv3.SetURL("http://127.0.0.1:9200"),
elasticv3.SetHttpClient(tc),
)
// Spans are emitted for all
client.Index().
Index("twitter").Type("tweet").Index("1").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.Background())
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/tweet/1")
ctx := root.Context(context.Background())
client.Get().
Index("twitter").Type("tweet").Index("1").
DoC(ctx)
root.Finish()
}

View file

@ -0,0 +1,26 @@
package elastictrace
import (
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"regexp"
)
var (
IdRegexp = regexp.MustCompile("/([0-9]+)([/\\?]|$)")
IdPlaceholder = []byte("/?$2")
IndexRegexp = regexp.MustCompile("[0-9]{2,}")
IndexPlaceholder = []byte("?")
)
// Quantize quantizes an Elasticsearch to extract a meaningful resource from the request.
// We quantize based on the method+url with some cleanup applied to the URL.
// URLs with an ID will be generalized as will (potential) timestamped indices.
func Quantize(span *tracer.Span) {
url := span.GetMeta("elasticsearch.url")
method := span.GetMeta("elasticsearch.method")
quantizedURL := IdRegexp.ReplaceAll([]byte(url), IdPlaceholder)
quantizedURL = IndexRegexp.ReplaceAll(quantizedURL, IndexPlaceholder)
span.Resource = fmt.Sprintf("%s %s", method, quantizedURL)
}

View file

@ -0,0 +1,43 @@
package elastictrace
import (
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/stretchr/testify/assert"
)
func TestQuantize(t *testing.T) {
tr := tracer.NewTracer()
for _, tc := range []struct {
url, method string
expected string
}{
{
url: "/twitter/tweets",
method: "POST",
expected: "POST /twitter/tweets",
},
{
url: "/logs_2016_05/event/_search",
method: "GET",
expected: "GET /logs_?_?/event/_search",
},
{
url: "/twitter/tweets/123",
method: "GET",
expected: "GET /twitter/tweets/?",
},
{
url: "/logs_2016_05/event/123",
method: "PUT",
expected: "PUT /logs_?_?/event/?",
},
} {
span := tracer.NewSpan("name", "elasticsearch", "", 0, 0, 0, tr)
span.SetMeta("elasticsearch.url", tc.url)
span.SetMeta("elasticsearch.method", tc.method)
Quantize(span)
assert.Equal(t, tc.expected, span.Resource)
}
}

View file

@ -0,0 +1,33 @@
require_relative 'common'
namespace :collect do
desc 'Run client benchmarks'
task :benchmarks do
# TODO: benchmarks must be done for different Tracer versions
# so that we can retrieve the diff and return an exit code != 0
Tasks::Common.get_go_packages.each do |pkg|
sh "go test -run=NONE -bench=. #{pkg}"
end
end
desc 'Run pprof to collect profiles'
task :profiles do
# initializes the folder to collect profiles
FileUtils.mkdir_p 'profiles'
filename = "#{Tasks::Common::PROFILES}/tracer"
# generate a profile for the Tracer based on benchmarks
sh %{
go test -run=NONE -bench=.
-cpuprofile=#{filename}-cpu.out
-memprofile=#{filename}-mem.out
-blockprofile=#{filename}-block.out
#{Tasks::Common::TRACER_PACKAGE}
}.gsub(/\s+/, ' ').strip
# export profiles
sh "go tool pprof -text -nodecount=10 -cum ./tracer.test #{filename}-cpu.out"
sh "go tool pprof -text -nodecount=10 -cum -inuse_space ./tracer.test #{filename}-mem.out"
sh "go tool pprof -text -nodecount=10 -cum ./tracer.test #{filename}-block.out"
end
end

12
vendor/github.com/DataDog/dd-trace-go/tasks/common.rb generated vendored Normal file
View file

@ -0,0 +1,12 @@
module Tasks
module Common
PROFILES = './profiles'
TRACER_PACKAGE = 'github.com/DataDog/dd-trace-go/tracer'
COVERAGE_FILE = 'code.cov'
# returns a list of Go packages
def self.get_go_packages
`go list ./opentracing ./tracer ./contrib/...`.split("\n")
end
end
end

41
vendor/github.com/DataDog/dd-trace-go/tasks/testing.rb generated vendored Normal file
View file

@ -0,0 +1,41 @@
require 'tempfile'
require_relative 'common'
namespace :test do
desc 'Run linting on the repository'
task :lint do
# enable-gc is required because with a full linting process we may finish workers memory
# fast is used temporarily for a faster CI
sh 'gometalinter --deadline 60s --fast --enable-gc --errors --vendor ./opentracing ./tracer ./contrib/...'
end
desc 'Test all packages'
task :all do
sh 'go test ./opentracing ./tracer ./contrib/...'
end
desc 'Test all packages with -race flag'
task :race do
sh 'go test -race ./opentracing ./tracer ./contrib/...'
end
desc 'Run test coverage'
task :coverage do
# collect global profiles in this file
sh "echo \"mode: count\" > #{Tasks::Common::COVERAGE_FILE}"
# for each package collect and append the profile
Tasks::Common.get_go_packages.each do |pkg|
begin
f = Tempfile.new('profile')
# run code coverage
sh "go test -short -covermode=count -coverprofile=#{f.path} #{pkg}"
sh "cat #{f.path} | tail -n +2 >> #{Tasks::Common::COVERAGE_FILE}"
ensure
File.delete(f)
end
end
sh "go tool cover -func #{Tasks::Common::COVERAGE_FILE}"
end
end

23
vendor/github.com/DataDog/dd-trace-go/tasks/vendors.rb generated vendored Normal file
View file

@ -0,0 +1,23 @@
desc 'Initialize the development environment'
task :init do
sh 'go get -u github.com/golang/dep/cmd/dep'
sh 'go get -u github.com/alecthomas/gometalinter'
sh 'gometalinter --install'
# TODO:bertrand remove this
# It is only a short-term workaround, we should find a proper way to handle
# multiple versions of the same dependency
sh 'go get -d google.golang.org/grpc'
gopath = ENV["GOPATH"].split(":")[0]
sh "cd #{gopath}/src/google.golang.org/grpc/ && git checkout v1.5.2 && cd -"
sh "go get -t -v ./contrib/..."
sh "go get -v github.com/opentracing/opentracing-go"
end
namespace :vendors do
desc "Update the vendors list"
task :update do
# download and update our vendors
sh 'dep ensure'
end
end

View file

@ -0,0 +1,3 @@
# [DEPRECATED] Libraries supported for tracing
This folder will be dropped on the medium-term. It's now located at [`/contrib`](https://github.com/DataDog/dd-trace-go/tree/master/contrib).

View file

@ -0,0 +1,86 @@
// Package elastictraced provides tracing for the Elastic Elasticsearch client.
// Supports v3 (gopkg.in/olivere/elastic.v3), v5 (gopkg.in/olivere/elastic.v5)
// but with v3 you must use `DoC` on all requests to capture the request context.
package elastictraced
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
)
// MaxContentLength is the maximum content length for which we'll read and capture
// the contents of the request body. Anything larger will still be traced but the
// body will not be captured as trace metadata.
const MaxContentLength = 500 * 1024
// TracedTransport is a traced HTTP transport that captures Elasticsearch spans.
type TracedTransport struct {
service string
tracer *tracer.Tracer
*http.Transport
}
// RoundTrip satisfies the RoundTripper interface, wraps the sub Transport and
// captures a span of the Elasticsearch request.
func (t *TracedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
span := t.tracer.NewChildSpanFromContext("elasticsearch.query", req.Context())
span.Service = t.service
span.Type = ext.AppTypeDB
defer span.Finish()
span.SetMeta("elasticsearch.method", req.Method)
span.SetMeta("elasticsearch.url", req.URL.Path)
span.SetMeta("elasticsearch.params", req.URL.Query().Encode())
contentLength, _ := strconv.Atoi(req.Header.Get("Content-Length"))
if req.Body != nil && contentLength < MaxContentLength {
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
span.SetMeta("elasticsearch.body", string(buf))
req.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
}
// Run the request using the standard transport.
res, err := t.Transport.RoundTrip(req)
if res != nil {
span.SetMeta(ext.HTTPCode, strconv.Itoa(res.StatusCode))
}
if err != nil {
span.SetError(err)
} else if res.StatusCode < 200 || res.StatusCode > 299 {
buf, err := ioutil.ReadAll(res.Body)
if err != nil {
// Status text is best we can do if if we can't read the body.
span.SetError(errors.New(http.StatusText(res.StatusCode)))
} else {
span.SetError(errors.New(string(buf)))
}
res.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
}
Quantize(span)
return res, err
}
// NewTracedHTTPClient returns a new TracedTransport that traces HTTP requests.
func NewTracedHTTPClient(service string, tracer *tracer.Tracer) *http.Client {
return &http.Client{
Transport: &TracedTransport{service, tracer, &http.Transport{}},
}
}
// NewTracedHTTPClientWithTransport returns a new TracedTransport that traces HTTP requests
// and takes in a Transport to use something other than the default.
func NewTracedHTTPClientWithTransport(service string, tracer *tracer.Tracer, transport *http.Transport) *http.Client {
return &http.Client{
Transport: &TracedTransport{service, tracer, transport},
}
}

View file

@ -0,0 +1,193 @@
package elastictraced
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/stretchr/testify/assert"
elasticv3 "gopkg.in/olivere/elastic.v3"
elasticv5 "gopkg.in/olivere/elastic.v5"
"testing"
)
const (
debug = false
)
func TestClientV5(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv5.NewClient(
elasticv5.SetURL("http://127.0.0.1:59200"),
elasticv5.SetHttpClient(tc),
elasticv5.SetSniff(false),
elasticv5.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.TODO())
assert.NoError(err)
checkPUTTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("twitter").Type("tweet").
Id("1").Do(context.TODO())
assert.NoError(err)
checkGETTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("not-real-index").
Id("1").Do(context.TODO())
assert.Error(err)
checkErrTrace(assert, testTracer, testTransport)
}
func TestClientV3(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv3.NewClient(
elasticv3.SetURL("http://127.0.0.1:59201"),
elasticv3.SetHttpClient(tc),
elasticv3.SetSniff(false),
elasticv3.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.TODO())
assert.NoError(err)
checkPUTTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("twitter").Type("tweet").
Id("1").DoC(context.TODO())
assert.NoError(err)
checkGETTrace(assert, testTracer, testTransport)
_, err = client.Get().Index("not-real-index").
Id("1").DoC(context.TODO())
assert.Error(err)
checkErrTrace(assert, testTracer, testTransport)
}
func TestClientV3Failure(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv3.NewClient(
// not existing service, it must fail
elasticv3.SetURL("http://127.0.0.1:29201"),
elasticv3.SetHttpClient(tc),
elasticv3.SetSniff(false),
elasticv3.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.TODO())
assert.Error(err)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*net.OpError", spans[0].GetMeta("error.type"))
}
func TestClientV5Failure(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
tc := NewTracedHTTPClient("my-es-service", testTracer)
client, err := elasticv5.NewClient(
// not existing service, it must fail
elasticv5.SetURL("http://127.0.0.1:29200"),
elasticv5.SetHttpClient(tc),
elasticv5.SetSniff(false),
elasticv5.SetHealthcheck(false),
)
assert.NoError(err)
_, err = client.Index().
Index("twitter").Id("1").
Type("tweet").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.TODO())
assert.Error(err)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*net.OpError", spans[0].GetMeta("error.type"))
}
func checkPUTTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("PUT /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("PUT", spans[0].GetMeta("elasticsearch.method"))
}
func checkGETTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("GET /twitter/tweet/?", spans[0].Resource)
assert.Equal("/twitter/tweet/1", spans[0].GetMeta("elasticsearch.url"))
assert.Equal("GET", spans[0].GetMeta("elasticsearch.method"))
}
func checkErrTrace(assert *assert.Assertions, tracer *tracer.Tracer, transport *tracertest.DummyTransport) {
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Equal("my-es-service", spans[0].Service)
assert.Equal("GET /not-real-index/_all/?", spans[0].Resource)
assert.Equal("/not-real-index/_all/1", spans[0].GetMeta("elasticsearch.url"))
assert.NotEmpty(spans[0].GetMeta("error.msg"))
assert.Equal("*errors.errorString", spans[0].GetMeta("error.type"))
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *tracertest.DummyTransport) {
transport := &tracertest.DummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}

View file

@ -0,0 +1,57 @@
package elastictraced_test
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/elastictraced"
elasticv3 "gopkg.in/olivere/elastic.v3"
elasticv5 "gopkg.in/olivere/elastic.v5"
)
// To start tracing elastic.v5 requests, create a new TracedHTTPClient that you will
// use when initializing the elastic.Client.
func Example_v5() {
tc := elastictraced.NewTracedHTTPClient("my-elasticsearch-service", tracer.DefaultTracer)
client, _ := elasticv5.NewClient(
elasticv5.SetURL("http://127.0.0.1:9200"),
elasticv5.SetHttpClient(tc),
)
// Spans are emitted for all
client.Index().
Index("twitter").Type("tweet").Index("1").
BodyString(`{"user": "test", "message": "hello"}`).
Do(context.Background())
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/tweet/1")
ctx := root.Context(context.Background())
client.Get().
Index("twitter").Type("tweet").Index("1").
Do(ctx)
root.Finish()
}
// To trace elastic.v3 you create a TracedHTTPClient in the same way but all requests must use
// the DoC() call to pass the request context.
func Example_v3() {
tc := elastictraced.NewTracedHTTPClient("my-elasticsearch-service", tracer.DefaultTracer)
client, _ := elasticv3.NewClient(
elasticv3.SetURL("http://127.0.0.1:9200"),
elasticv3.SetHttpClient(tc),
)
// Spans are emitted for all
client.Index().
Index("twitter").Type("tweet").Index("1").
BodyString(`{"user": "test", "message": "hello"}`).
DoC(context.Background())
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/tweet/1")
ctx := root.Context(context.Background())
client.Get().
Index("twitter").Type("tweet").Index("1").
DoC(ctx)
root.Finish()
}

View file

@ -0,0 +1,26 @@
package elastictraced
import (
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"regexp"
)
var (
IdRegexp = regexp.MustCompile("/([0-9]+)([/\\?]|$)")
IdPlaceholder = []byte("/?$2")
IndexRegexp = regexp.MustCompile("[0-9]{2,}")
IndexPlaceholder = []byte("?")
)
// Quantize quantizes an Elasticsearch to extract a meaningful resource from the request.
// We quantize based on the method+url with some cleanup applied to the URL.
// URLs with an ID will be generalized as will (potential) timestamped indices.
func Quantize(span *tracer.Span) {
url := span.GetMeta("elasticsearch.url")
method := span.GetMeta("elasticsearch.method")
quantizedURL := IdRegexp.ReplaceAll([]byte(url), IdPlaceholder)
quantizedURL = IndexRegexp.ReplaceAll(quantizedURL, IndexPlaceholder)
span.Resource = fmt.Sprintf("%s %s", method, quantizedURL)
}

View file

@ -0,0 +1,43 @@
package elastictraced
import (
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/stretchr/testify/assert"
)
func TestQuantize(t *testing.T) {
tr := tracer.NewTracer()
for _, tc := range []struct {
url, method string
expected string
}{
{
url: "/twitter/tweets",
method: "POST",
expected: "POST /twitter/tweets",
},
{
url: "/logs_2016_05/event/_search",
method: "GET",
expected: "GET /logs_?_?/event/_search",
},
{
url: "/twitter/tweets/123",
method: "GET",
expected: "GET /twitter/tweets/?",
},
{
url: "/logs_2016_05/event/123",
method: "PUT",
expected: "PUT /logs_?_?/event/?",
},
} {
span := tracer.NewSpan("name", "elasticsearch", "", 0, 0, 0, tr)
span.SetMeta("elasticsearch.url", tc.url)
span.SetMeta("elasticsearch.method", tc.method)
Quantize(span)
assert.Equal(t, tc.expected, span.Resource)
}
}

View file

@ -0,0 +1,59 @@
package gintrace_test
import (
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/gin-gonic/gintrace"
"github.com/gin-gonic/gin"
)
// To start tracing requests, add the trace middleware to your Gin router.
func Example() {
// Create your router and use the middleware.
r := gin.New()
r.Use(gintrace.Middleware("my-web-app"))
r.GET("/hello", func(c *gin.Context) {
c.String(200, "hello world!")
})
// Profit!
r.Run(":8080")
}
func ExampleHTML() {
r := gin.Default()
r.Use(gintrace.Middleware("my-web-app"))
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
// This will render the html and trace the execution time.
gintrace.HTML(c, 200, "index.tmpl", gin.H{
"title": "Main website",
})
})
}
func ExampleSpanDefault() {
r := gin.Default()
r.Use(gintrace.Middleware("image-encoder"))
r.GET("/image/encode", func(c *gin.Context) {
// The middleware patches a span to the request. Let's add some metadata,
// and create a child span.
span := gintrace.SpanDefault(c)
span.SetMeta("user.handle", "admin")
span.SetMeta("user.id", "1234")
encodeSpan := tracer.NewChildSpan("image.encode", span)
// encode a image
encodeSpan.Finish()
uploadSpan := tracer.NewChildSpan("image.upload", span)
// upload the image
uploadSpan.Finish()
c.String(200, "ok!")
})
}

View file

@ -0,0 +1,143 @@
// Package gintrace provides tracing middleware for the Gin web framework.
package gintrace
import (
"fmt"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gin-gonic/gin"
)
// key is the string that we'll use to store spans in the tracer.
var key = "datadog_trace_span"
// Middleware returns middleware that will trace requests with the default
// tracer.
func Middleware(service string) gin.HandlerFunc {
return MiddlewareTracer(service, tracer.DefaultTracer)
}
// MiddlewareTracer returns middleware that will trace requests with the given
// tracer.
func MiddlewareTracer(service string, t *tracer.Tracer) gin.HandlerFunc {
t.SetServiceInfo(service, "gin-gonic", ext.AppTypeWeb)
mw := newMiddleware(service, t)
return mw.Handle
}
// middleware implements gin middleware.
type middleware struct {
service string
trc *tracer.Tracer
}
func newMiddleware(service string, trc *tracer.Tracer) *middleware {
return &middleware{
service: service,
trc: trc,
}
}
// Handle is a gin HandlerFunc that will add tracing to the given request.
func (m *middleware) Handle(c *gin.Context) {
// bail if not enabled
if !m.trc.Enabled() {
c.Next()
return
}
// FIXME[matt] the handler name is a bit unwieldy and uses reflection
// under the hood. might be better to tackle this task and do it right
// so we can end up with "user/:user/whatever" instead of
// "github.com/foobar/blah"
//
// See here: https://github.com/gin-gonic/gin/issues/649
resource := c.HandlerName()
// Create our span and patch it to the context for downstream.
span := m.trc.NewRootSpan("gin.request", m.service, resource)
c.Set(key, span)
// Pass along the request.
c.Next()
// Set http tags.
span.SetMeta(ext.HTTPCode, strconv.Itoa(c.Writer.Status()))
span.SetMeta(ext.HTTPMethod, c.Request.Method)
span.SetMeta(ext.HTTPURL, c.Request.URL.Path)
// Set any error information.
var err error
if len(c.Errors) > 0 {
span.SetMeta("gin.errors", c.Errors.String()) // set all errors
err = c.Errors[0] // but use the first for standard fields
}
span.FinishWithErr(err)
}
// Span returns the Span stored in the given Context and true. If it doesn't exist,
// it will returns (nil, false)
func Span(c *gin.Context) (*tracer.Span, bool) {
if c == nil {
return nil, false
}
s, ok := c.Get(key)
if !ok {
return nil, false
}
switch span := s.(type) {
case *tracer.Span:
return span, true
}
return nil, false
}
// SpanDefault returns the span stored in the given Context. If none exists,
// it will return an empty span.
func SpanDefault(c *gin.Context) *tracer.Span {
span, ok := Span(c)
if !ok {
return &tracer.Span{}
}
return span
}
// NewChildSpan will create a span that is the child of the span stored in
// the context.
func NewChildSpan(name string, c *gin.Context) *tracer.Span {
span, ok := Span(c)
if !ok {
return &tracer.Span{}
}
return span.Tracer().NewChildSpan(name, span)
}
// HTML will trace the rendering of the template as a child of the span in the
// given context.
func HTML(c *gin.Context, code int, name string, obj interface{}) {
span, _ := Span(c)
if span == nil {
c.HTML(code, name, obj)
return
}
child := span.Tracer().NewChildSpan("gin.render.html", span)
child.SetMeta("go.template", name)
defer func() {
if r := recover(); r != nil {
err := fmt.Errorf("error rendering tmpl:%s: %s", name, r)
child.FinishWithErr(err)
panic(r)
} else {
child.Finish()
}
}()
// render
c.HTML(code, name, obj)
}

View file

@ -0,0 +1,250 @@
package gintrace
import (
"errors"
"fmt"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func init() {
gin.SetMode(gin.ReleaseMode) // silence annoying log msgs
}
func TestChildSpan(t *testing.T) {
assert := assert.New(t)
testTracer, _ := getTestTracer()
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/user/:id", func(c *gin.Context) {
span, ok := tracer.SpanFromContext(c)
assert.True(ok)
assert.NotNil(span)
})
r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
}
func TestTrace200(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/user/:id", func(c *gin.Context) {
// assert we patch the span on the request context.
span := SpanDefault(c)
span.SetMeta("test.gin", "ginny")
assert.Equal(span.Service, "foobar")
id := c.Param("id")
c.Writer.Write([]byte(id))
})
r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()
// do and verify the request
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
// verify traces look good
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
if len(spans) < 1 {
t.Fatalf("no spans")
}
s := spans[0]
assert.Equal(s.Service, "foobar")
assert.Equal(s.Name, "gin.request")
// FIXME[matt] would be much nicer to have "/user/:id" here
assert.True(strings.Contains(s.Resource, "gintrace.TestTrace200"))
assert.Equal(s.GetMeta("test.gin"), "ginny")
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), "/user/123")
}
func TestDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetEnabled(false)
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
router.GET("/ping", func(c *gin.Context) {
span, ok := Span(c)
assert.Nil(span)
assert.False(ok)
c.Writer.Write([]byte("ok"))
})
r := httptest.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
// do and verify the request
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
// verify traces look good
testTracer.ForceFlush()
spans := testTransport.Traces()
assert.Len(spans, 0)
}
func TestError(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
// setup
middleware := newMiddleware("foobar", testTracer)
router := gin.New()
router.Use(middleware.Handle)
// a handler with an error and make the requests
router.GET("/err", func(c *gin.Context) {
c.AbortWithError(500, errors.New("oh no"))
})
r := httptest.NewRequest("GET", "/err", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 500)
// verify the errors and status are correct
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
if len(spans) < 1 {
t.Fatalf("no spans")
}
s := spans[0]
assert.Equal(s.Service, "foobar")
assert.Equal(s.Name, "gin.request")
assert.Equal(s.GetMeta("http.status_code"), "500")
assert.Equal(s.GetMeta(ext.ErrorMsg), "oh no")
assert.Equal(s.Error, int32(1))
}
func TestHTML(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
// setup
middleware := newMiddleware("tmplservice", testTracer)
router := gin.New()
router.Use(middleware.Handle)
// add a template
tmpl := template.Must(template.New("hello").Parse("hello {{.}}"))
router.SetHTMLTemplate(tmpl)
// a handler with an error and make the requests
router.GET("/hello", func(c *gin.Context) {
HTML(c, 200, "hello", "world")
})
r := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
assert.Equal("hello world", w.Body.String())
// verify the errors and status are correct
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
for _, s := range spans {
assert.Equal(s.Service, "tmplservice")
}
var tspan *tracer.Span
for _, s := range spans {
// we need to pick up the span we're searching for, as the
// order is not garanteed within the buffer
if s.Name == "gin.render.html" {
tspan = s
}
}
assert.NotNil(tspan, "we should have found a span with name gin.render.html")
assert.Equal(tspan.GetMeta("go.template"), "hello")
fmt.Println(spans)
}
func TestGetSpanNotInstrumented(t *testing.T) {
assert := assert.New(t)
router := gin.New()
router.GET("/ping", func(c *gin.Context) {
// Assert we don't have a span on the context.
s, ok := Span(c)
assert.False(ok)
assert.Nil(s)
// and the default span is empty
s = SpanDefault(c)
assert.Equal(s.Service, "")
c.Writer.Write([]byte("ok"))
})
r := httptest.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(response.StatusCode, 200)
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -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")
// pipe 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()
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,228 @@
package goredistrace
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/go-redis/redis"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"time"
)
const (
debug = false
)
func TestClient(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default db
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
client.Set("test_key", "test_value", 0)
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "set test_key test_value: ")
assert.Equal(span.GetMeta("redis.args_length"), "3")
}
func TestPipeline(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default db
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
pipeline := client.Pipeline()
pipeline.Expire("pipeline_counter", time.Hour)
// Exec with context test
pipeline.ExecWithContext(context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.pipeline_length"), "1")
assert.Equal(span.Resource, "expire pipeline_counter 3600: false\n")
pipeline.Expire("pipeline_counter", time.Hour)
pipeline.Expire("pipeline_counter_1", time.Minute)
// Rewriting Exec
pipeline.Exec()
testTracer.ForceFlush()
traces = testTransport.Traces()
assert.Len(traces, 1)
spans = traces[0]
assert.Len(spans, 1)
span = spans[0]
assert.Equal(span.Service, "my-redis")
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("redis.pipeline_length"), "2")
assert.Equal(span.Resource, "expire pipeline_counter 3600: false\nexpire pipeline_counter_1 60: false\n")
}
func TestChildSpan(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parent_span := testTracer.NewChildSpanFromContext("parent_span", ctx)
ctx = tracer.ContextWithSpan(ctx, parent_span)
client := NewTracedClient(opts, testTracer, "my-redis")
client.SetContext(ctx)
client.Set("test_key", "test_value", 0)
parent_span.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var child_span, pspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "redis.command":
child_span = s
case "parent_span":
pspan = s
}
}
assert.NotNil(child_span, "there should be a child redis.command span")
assert.NotNil(child_span, "there should be a parent span")
assert.Equal(child_span.ParentID, pspan.SpanID)
assert.Equal(child_span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(child_span.GetMeta("out.port"), "56379")
}
func TestMultipleCommands(t *testing.T) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
client.Set("test_key", "test_value", 0)
client.Get("test_key")
client.Incr("int_key")
client.ClientList()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 4)
spans := traces[0]
assert.Len(spans, 1)
// Checking all commands were recorded
var commands [4]string
for i := 0; i < 4; i++ {
commands[i] = traces[i][0].GetMeta("redis.raw_command")
}
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) {
opts := &redis.Options{
Addr: "127.0.0.1:56379",
Password: "", // no password set
DB: 0, // use default DB
}
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
client := NewTracedClient(opts, testTracer, "my-redis")
err := client.Get("non_existent_key")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Err().Error())
assert.Equal(span.Name, "redis.command")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "get non_existent_key: ")
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,27 @@
package gocqltrace
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gocql/gocql"
)
// To trace Cassandra commands, use our query wrapper TraceQuery.
func Example() {
// Initialise a Cassandra session as usual, create a query.
cluster := gocql.NewCluster("127.0.0.1")
session, _ := cluster.CreateSession()
query := session.Query("CREATE KEYSPACE if not exists trace WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1}")
// Use context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// Wrap the query to trace it and pass the context for inheritance
tracedQuery := TraceQuery("ServiceName", tracer.DefaultTracer, query)
tracedQuery.WithContext(ctx)
// Execute your query as usual
tracedQuery.Exec()
}

View file

@ -0,0 +1,146 @@
// Package gocqltrace provides tracing for the Cassandra Gocql client (https://github.com/gocql/gocql)
package gocqltrace
import (
"context"
"strconv"
"strings"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gocql/gocql"
)
// TracedQuery inherits from gocql.Query, it keeps the tracer and the context.
type TracedQuery struct {
*gocql.Query
p traceParams
traceContext context.Context
}
// TracedIter inherits from gocql.Iter and contains a span.
type TracedIter struct {
*gocql.Iter
span *tracer.Span
}
// traceParams containes fields and metadata useful for command tracing
type traceParams struct {
tracer *tracer.Tracer
service string
keyspace string
paginated string
consistancy string
query string
}
// TraceQuery wraps a gocql.Query into a TracedQuery
func TraceQuery(service string, tracer *tracer.Tracer, q *gocql.Query) *TracedQuery {
stringQuery := `"` + strings.SplitN(q.String(), "\"", 3)[1] + `"`
stringQuery, err := strconv.Unquote(stringQuery)
if err != nil {
// An invalid string, so that the trace is not dropped
// due to having an empty resource
stringQuery = "_"
}
tq := &TracedQuery{q, traceParams{tracer, service, "", "false", strconv.Itoa(int(q.GetConsistency())), stringQuery}, context.Background()}
tracer.SetServiceInfo(service, ext.CassandraType, ext.AppTypeDB)
return tq
}
// WithContext rewrites the original function so that ctx can be used for inheritance
func (tq *TracedQuery) WithContext(ctx context.Context) *TracedQuery {
tq.traceContext = ctx
tq.Query.WithContext(ctx)
return tq
}
// PageState rewrites the original function so that spans are aware of the change.
func (tq *TracedQuery) PageState(state []byte) *TracedQuery {
tq.p.paginated = "true"
tq.Query = tq.Query.PageState(state)
return tq
}
// NewChildSpan creates a new span from the traceParams and the context.
func (tq *TracedQuery) NewChildSpan(ctx context.Context) *tracer.Span {
span := tq.p.tracer.NewChildSpanFromContext(ext.CassandraQuery, ctx)
span.Type = ext.CassandraType
span.Service = tq.p.service
span.Resource = tq.p.query
span.SetMeta(ext.CassandraPaginated, tq.p.paginated)
span.SetMeta(ext.CassandraKeyspace, tq.p.keyspace)
return span
}
// Exec is rewritten so that it passes by our custom Iter
func (tq *TracedQuery) Exec() error {
return tq.Iter().Close()
}
// MapScan wraps in a span query.MapScan call.
func (tq *TracedQuery) MapScan(m map[string]interface{}) error {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
err := tq.Query.MapScan(m)
if err != nil {
span.SetError(err)
}
return err
}
// Scan wraps in a span query.Scan call.
func (tq *TracedQuery) Scan(dest ...interface{}) error {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
err := tq.Query.Scan(dest...)
if err != nil {
span.SetError(err)
}
return err
}
// ScanCAS wraps in a span query.ScanCAS call.
func (tq *TracedQuery) ScanCAS(dest ...interface{}) (applied bool, err error) {
span := tq.NewChildSpan(tq.traceContext)
defer span.Finish()
applied, err = tq.Query.ScanCAS(dest...)
if err != nil {
span.SetError(err)
}
return applied, err
}
// Iter starts a new span at query.Iter call.
func (tq *TracedQuery) Iter() *TracedIter {
span := tq.NewChildSpan(tq.traceContext)
iter := tq.Query.Iter()
span.SetMeta(ext.CassandraRowCount, strconv.Itoa(iter.NumRows()))
span.SetMeta(ext.CassandraConsistencyLevel, strconv.Itoa(int(tq.GetConsistency())))
columns := iter.Columns()
if len(columns) > 0 {
span.SetMeta(ext.CassandraKeyspace, columns[0].Keyspace)
} else {
}
tIter := &TracedIter{iter, span}
if tIter.Host() != nil {
tIter.span.SetMeta(ext.TargetHost, tIter.Iter.Host().HostID())
tIter.span.SetMeta(ext.TargetPort, strconv.Itoa(tIter.Iter.Host().Port()))
tIter.span.SetMeta(ext.CassandraCluster, tIter.Iter.Host().DataCenter())
}
return tIter
}
// Close closes the TracedIter and finish the span created on Iter call.
func (tIter *TracedIter) Close() error {
err := tIter.Iter.Close()
if err != nil {
tIter.span.SetError(err)
}
tIter.span.Finish()
return err
}

View file

@ -0,0 +1,132 @@
package gocqltrace
import (
"context"
"net/http"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
)
const (
debug = false
)
// TestMain sets up the Keyspace and table if they do not exist
func TestMain(m *testing.M) {
cluster := gocql.NewCluster("127.0.0.1:59042")
session, _ := cluster.CreateSession()
// Ensures test keyspace and table person exists.
session.Query("CREATE KEYSPACE if not exists trace WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1}").Exec()
session.Query("CREATE TABLE if not exists trace.person (name text PRIMARY KEY, age int, description text)").Exec()
session.Query("INSERT INTO trace.person (name, age, description) VALUES ('Cassandra', 100, 'A cruel mistress')").Exec()
m.Run()
}
func TestErrorWrapper(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
cluster := gocql.NewCluster("127.0.0.1:59042")
session, _ := cluster.CreateSession()
q := session.Query("CREATE KEYSPACE trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
err := TraceQuery("ServiceName", testTracer, q).Exec()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Error())
assert.Equal(span.Name, ext.CassandraQuery)
assert.Equal(span.Resource, "CREATE KEYSPACE trace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 1 };")
assert.Equal(span.Service, "ServiceName")
assert.Equal(span.GetMeta(ext.CassandraConsistencyLevel), "4")
assert.Equal(span.GetMeta(ext.CassandraPaginated), "false")
// Not added in case of an error
assert.Equal(span.GetMeta(ext.TargetHost), "")
assert.Equal(span.GetMeta(ext.TargetPort), "")
assert.Equal(span.GetMeta(ext.CassandraCluster), "")
assert.Equal(span.GetMeta(ext.CassandraKeyspace), "")
}
func TestChildWrapperSpan(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parentSpan := testTracer.NewChildSpanFromContext("parentSpan", ctx)
ctx = tracer.ContextWithSpan(ctx, parentSpan)
cluster := gocql.NewCluster("127.0.0.1:59042")
session, _ := cluster.CreateSession()
q := session.Query("SELECT * from trace.person")
tq := TraceQuery("TestServiceName", testTracer, q)
tq.WithContext(ctx).Exec()
parentSpan.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var childSpan, pSpan *tracer.Span
if spans[0].ParentID == spans[1].SpanID {
childSpan = spans[0]
pSpan = spans[1]
} else {
childSpan = spans[1]
pSpan = spans[0]
}
assert.Equal(pSpan.Name, "parentSpan")
assert.Equal(childSpan.ParentID, pSpan.SpanID)
assert.Equal(childSpan.Name, ext.CassandraQuery)
assert.Equal(childSpan.Resource, "SELECT * from trace.person")
assert.Equal(childSpan.GetMeta(ext.CassandraKeyspace), "trace")
assert.Equal(childSpan.GetMeta(ext.TargetPort), "59042")
assert.Equal(childSpan.GetMeta(ext.TargetHost), "127.0.0.1")
assert.Equal(childSpan.GetMeta(ext.CassandraCluster), "datacenter1")
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,31 @@
package muxtrace_test
import (
"fmt"
"net/http"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/gorilla/muxtrace"
"github.com/gorilla/mux"
)
// handler is a simple handlerFunc that logs some data from the span
// that is injected into the requests' context.
func handler(w http.ResponseWriter, r *http.Request) {
span := tracer.SpanFromContextDefault(r.Context())
fmt.Printf("tracing service:%s resource:%s", span.Service, span.Resource)
w.Write([]byte("hello world"))
}
func Example() {
router := mux.NewRouter()
muxTracer := muxtrace.NewMuxTracer("my-web-app", tracer.DefaultTracer)
// Add traced routes directly.
muxTracer.HandleFunc(router, "/users", handler)
// and subroutes as well.
subrouter := router.PathPrefix("/user").Subrouter()
muxTracer.HandleFunc(subrouter, "/view", handler)
muxTracer.HandleFunc(subrouter, "/create", handler)
}

View file

@ -0,0 +1,127 @@
// Package muxtrace provides tracing functions for the Gorilla Mux framework.
package muxtrace
import (
"net/http"
"strconv"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/gorilla/mux"
)
// MuxTracer is used to trace requests in a mux server.
type MuxTracer struct {
tracer *tracer.Tracer
service string
}
// NewMuxTracer creates a MuxTracer for the given service and tracer.
func NewMuxTracer(service string, t *tracer.Tracer) *MuxTracer {
t.SetServiceInfo(service, "gorilla", ext.AppTypeWeb)
return &MuxTracer{
tracer: t,
service: service,
}
}
// TraceHandleFunc will return a HandlerFunc that will wrap tracing around the
// given handler func.
func (m *MuxTracer) TraceHandleFunc(handler http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, req *http.Request) {
// bail our if tracing isn't enabled.
if !m.tracer.Enabled() {
handler(writer, req)
return
}
// trace the request
tracedRequest, span := m.trace(req)
defer span.Finish()
// trace the response
tracedWriter := newTracedResponseWriter(span, writer)
// run the request
handler(tracedWriter, tracedRequest)
}
}
// HandleFunc will add a traced version of the given handler to the router.
func (m *MuxTracer) HandleFunc(router *mux.Router, pattern string, handler http.HandlerFunc) *mux.Route {
return router.HandleFunc(pattern, m.TraceHandleFunc(handler))
}
// span will create a span for the given request.
func (m *MuxTracer) trace(req *http.Request) (*http.Request, *tracer.Span) {
route := mux.CurrentRoute(req)
path, err := route.GetPathTemplate()
if err != nil {
// when route doesn't define a path
path = "unknown"
}
resource := req.Method + " " + path
span := m.tracer.NewRootSpan("mux.request", m.service, resource)
span.Type = ext.HTTPType
span.SetMeta(ext.HTTPMethod, req.Method)
span.SetMeta(ext.HTTPURL, path)
// patch the span onto the request context.
treq := SetRequestSpan(req, span)
return treq, span
}
// tracedResponseWriter is a small wrapper around an http response writer that will
// intercept and store the status of a request.
type tracedResponseWriter struct {
span *tracer.Span
w http.ResponseWriter
status int
}
func newTracedResponseWriter(span *tracer.Span, w http.ResponseWriter) *tracedResponseWriter {
return &tracedResponseWriter{
span: span,
w: w}
}
func (t *tracedResponseWriter) Header() http.Header {
return t.w.Header()
}
func (t *tracedResponseWriter) Write(b []byte) (int, error) {
if t.status == 0 {
t.WriteHeader(http.StatusOK)
}
return t.w.Write(b)
}
func (t *tracedResponseWriter) WriteHeader(status int) {
t.w.WriteHeader(status)
t.status = status
t.span.SetMeta(ext.HTTPCode, strconv.Itoa(status))
if status >= 500 && status < 600 {
t.span.Error = 1
}
}
// SetRequestSpan sets the span on the request's context.
func SetRequestSpan(r *http.Request, span *tracer.Span) *http.Request {
if r == nil || span == nil {
return r
}
ctx := tracer.ContextWithSpan(r.Context(), span)
return r.WithContext(ctx)
}
// GetRequestSpan will return the span associated with the given request. It
// will return nil/false if it doesn't exist.
func GetRequestSpan(r *http.Request) (*tracer.Span, bool) {
span, ok := tracer.SpanFromContext(r.Context())
return span, ok
}

View file

@ -0,0 +1,206 @@
package muxtrace
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestMuxTracerDisabled(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport, muxTracer := getTestTracer("disabled-service")
router := mux.NewRouter()
muxTracer.HandleFunc(router, "/disabled", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("disabled!"))
assert.Nil(err)
// Ensure we have no tracing context.
span, ok := tracer.SpanFromContext(r.Context())
assert.Nil(span)
assert.False(ok)
})
testTracer.SetEnabled(false) // the key line in this test.
// make the request
req := httptest.NewRequest("GET", "/disabled", nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "disabled!")
// assert nothing was traced.
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 0)
}
func TestMuxTracerSubrequest(t *testing.T) {
assert := assert.New(t)
// Send and verify a 200 request
for _, url := range []string{"/sub/child1", "/sub/child2"} {
tracer, transport, router := setup(t)
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "200!")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(0))
}
}
func TestMuxTracer200(t *testing.T) {
assert := assert.New(t)
// setup
tracer, transport, router := setup(t)
// Send and verify a 200 request
url := "/200"
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 200)
assert.Equal(writer.Body.String(), "200!")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "200")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(0))
}
func TestMuxTracer500(t *testing.T) {
assert := assert.New(t)
// setup
tracer, transport, router := setup(t)
// SEnd and verify a 200 request
url := "/500"
req := httptest.NewRequest("GET", url, nil)
writer := httptest.NewRecorder()
router.ServeHTTP(writer, req)
assert.Equal(writer.Code, 500)
assert.Equal(writer.Body.String(), "500!\n")
// ensure properly traced
tracer.ForceFlush()
traces := transport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
s := spans[0]
assert.Equal(s.Name, "mux.request")
assert.Equal(s.Service, "my-service")
assert.Equal(s.Resource, "GET "+url)
assert.Equal(s.GetMeta("http.status_code"), "500")
assert.Equal(s.GetMeta("http.method"), "GET")
assert.Equal(s.GetMeta("http.url"), url)
assert.Equal(s.Error, int32(1))
}
// test handlers
func handler200(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("200!"))
assert.Nil(err)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal(span.Service, "my-service")
assert.Equal(span.Duration, int64(0))
}
}
func handler500(t *testing.T) http.HandlerFunc {
assert := assert.New(t)
return func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500!", http.StatusInternalServerError)
span := tracer.SpanFromContextDefault(r.Context())
assert.Equal(span.Service, "my-service")
assert.Equal(span.Duration, int64(0))
}
}
func setup(t *testing.T) (*tracer.Tracer, *dummyTransport, *mux.Router) {
tracer, transport, mt := getTestTracer("my-service")
r := mux.NewRouter()
h200 := handler200(t)
h500 := handler500(t)
// Ensure we can use HandleFunc and it returns a route
mt.HandleFunc(r, "/200", h200).Methods("Get")
// And we can allso handle a bare func
r.HandleFunc("/500", mt.TraceHandleFunc(h500))
// do a subrouter (one in each way)
sub := r.PathPrefix("/sub").Subrouter()
sub.HandleFunc("/child1", mt.TraceHandleFunc(h200))
mt.HandleFunc(sub, "/child2", h200)
return tracer, transport, r
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer(service string) (*tracer.Tracer, *dummyTransport, *MuxTracer) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
muxTracer := NewMuxTracer(service, tracer)
return tracer, transport, muxTracer
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,59 @@
package redigotrace_test
import (
"context"
"github.com/DataDog/dd-trace-go/tracer"
redigotrace "github.com/DataDog/dd-trace-go/tracer/contrib/redigo"
"github.com/garyburd/redigo/redis"
)
// To start tracing Redis commands, use the TracedDial function to create a connection,
// passing in a service name of choice.
func Example() {
c, _ := redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
// Emit spans per command by using your Redis connection as usual
c.Do("SET", "vehicle", "truck")
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When passed a context as the final argument, c.Do will emit a span inheriting from 'parent.request'
c.Do("SET", "food", "cheese", ctx)
root.Finish()
}
func ExampleTracedConn() {
c, _ := redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
// Emit spans per command by using your Redis connection as usual
c.Do("SET", "vehicle", "truck")
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When passed a context as the final argument, c.Do will emit a span inheriting from 'parent.request'
c.Do("SET", "food", "cheese", ctx)
root.Finish()
}
// Alternatively, provide a redis URL to the TracedDialURL function
func Example_dialURL() {
c, _ := redigotrace.TracedDialURL("my-redis-backend", tracer.DefaultTracer, "redis://127.0.0.1:6379")
c.Do("SET", "vehicle", "truck")
}
// When using a redigo Pool, set your Dial function to return a traced connection
func Example_pool() {
pool := &redis.Pool{
Dial: func() (redis.Conn, error) {
return redigotrace.TracedDial("my-redis-backend", tracer.DefaultTracer, "tcp", "127.0.0.1:6379")
},
}
c := pool.Get()
c.Do("SET", " whiskey", " glass")
}

View file

@ -0,0 +1,131 @@
// Package redigotrace provides tracing for the Redigo Redis client (https://github.com/garyburd/redigo)
package redigotrace
import (
"bytes"
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
redis "github.com/garyburd/redigo/redis"
"net"
"net/url"
"strconv"
"strings"
)
// TracedConn is an implementation of the redis.Conn interface that supports tracing
type TracedConn struct {
redis.Conn
p traceParams
}
// traceParams contains fields and metadata useful for command tracing
type traceParams struct {
tracer *tracer.Tracer
service string
network string
host string
port string
}
// TracedDial takes a Conn returned by redis.Dial and configures it to emit spans with the given service name
func TracedDial(service string, tracer *tracer.Tracer, network, address string, options ...redis.DialOption) (redis.Conn, error) {
c, err := redis.Dial(network, address, options...)
addr := strings.Split(address, ":")
var host, port string
if len(addr) == 2 && addr[1] != "" {
port = addr[1]
} else {
port = "6379"
}
host = addr[0]
tracer.SetServiceInfo(service, "redis", ext.AppTypeDB)
tc := TracedConn{c, traceParams{tracer, service, network, host, port}}
return tc, err
}
// TracedDialURL takes a Conn returned by redis.DialURL and configures it to emit spans with the given service name
func TracedDialURL(service string, tracer *tracer.Tracer, rawurl string, options ...redis.DialOption) (redis.Conn, error) {
u, err := url.Parse(rawurl)
if err != nil {
return TracedConn{}, err
}
// Getting host and port, usind code from https://github.com/garyburd/redigo/blob/master/redis/conn.go#L226
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Host
port = "6379"
}
if host == "" {
host = "localhost"
}
// Set in redis.DialUrl source code
network := "tcp"
c, err := redis.DialURL(rawurl, options...)
tc := TracedConn{c, traceParams{tracer, service, 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 TracedConn) NewChildSpan(ctx context.Context) *tracer.Span {
span := tc.p.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = tc.p.service
span.SetMeta("out.network", tc.p.network)
span.SetMeta("out.port", tc.p.port)
span.SetMeta("out.host", tc.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 TracedConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
var ctx context.Context
var ok bool
if len(args) > 0 {
ctx, ok = args[len(args)-1].(context.Context)
if ok {
args = args[:len(args)-1]
}
}
span := tc.NewChildSpan(ctx)
defer func() {
if err != nil {
span.SetError(err)
}
span.Finish()
}()
span.SetMeta("redis.args_length", strconv.Itoa(len(args)))
if len(commandName) > 0 {
span.Resource = 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.Resource = "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.SetMeta("redis.raw_command", b.String())
return tc.Conn.Do(commandName, args...)
}

View file

@ -0,0 +1,214 @@
package redigotrace
import (
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/garyburd/redigo/redis"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
const (
debug = false
)
func TestClient(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
c.Do("SET", 1, "truck")
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "SET")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "SET 1 truck")
assert.Equal(span.GetMeta("redis.args_length"), "2")
}
func TestCommandError(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
_, err := c.Do("NOT_A_COMMAND", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(int32(span.Error), int32(1))
assert.Equal(span.GetMeta("error.msg"), err.Error())
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "NOT_A_COMMAND")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "NOT_A_COMMAND")
}
func TestConnectionError(t *testing.T) {
assert := assert.New(t)
testTracer, _ := getTestTracer()
testTracer.SetDebugLogging(debug)
_, err := TracedDial("redis-service", testTracer, "tcp", "127.0.0.1:1000")
assert.Contains(err.Error(), "dial tcp 127.0.0.1:1000")
}
func TestInheritance(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
// Parent span
ctx := context.Background()
parent_span := testTracer.NewChildSpanFromContext("parent_span", ctx)
ctx = tracer.ContextWithSpan(ctx, parent_span)
client, _ := TracedDial("my_service", testTracer, "tcp", "127.0.0.1:56379")
client.Do("SET", "water", "bottle", ctx)
parent_span.Finish()
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 2)
var child_span, pspan *tracer.Span
for _, s := range spans {
// order of traces in buffer is not garanteed
switch s.Name {
case "redis.command":
child_span = s
case "parent_span":
pspan = s
}
}
assert.NotNil(child_span, "there should be a child redis.command span")
assert.NotNil(child_span, "there should be a parent span")
assert.Equal(child_span.ParentID, pspan.SpanID)
assert.Equal(child_span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(child_span.GetMeta("out.port"), "56379")
}
func TestCommandsToSring(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
stringify_test := TestStruct{Cpython: 57, Cgo: 8}
c, _ := TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
c.Do("SADD", "testSet", "a", int(0), int32(1), int64(2), stringify_test, context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.Name, "redis.command")
assert.Equal(span.Service, "my-service")
assert.Equal(span.Resource, "SADD")
assert.Equal(span.GetMeta("out.host"), "127.0.0.1")
assert.Equal(span.GetMeta("out.port"), "56379")
assert.Equal(span.GetMeta("redis.raw_command"), "SADD testSet a 0 1 2 [57, 8]")
}
func TestPool(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
pool := &redis.Pool{
MaxIdle: 2,
MaxActive: 3,
IdleTimeout: 23,
Wait: true,
Dial: func() (redis.Conn, error) {
return TracedDial("my-service", testTracer, "tcp", "127.0.0.1:56379")
},
}
pc := pool.Get()
pc.Do("SET", " whiskey", " glass", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
spans := traces[0]
assert.Len(spans, 1)
span := spans[0]
assert.Equal(span.GetMeta("out.network"), "tcp")
}
func TestTracingDialUrl(t *testing.T) {
assert := assert.New(t)
testTracer, testTransport := getTestTracer()
testTracer.SetDebugLogging(debug)
url := "redis://127.0.0.1:56379"
client, _ := TracedDialURL("redis-service", testTracer, url)
client.Do("SET", "ONE", " TWO", context.Background())
testTracer.ForceFlush()
traces := testTransport.Traces()
assert.Len(traces, 1)
}
// TestStruct implements String interface
type TestStruct struct {
Cpython int
Cgo int
}
func (ts TestStruct) String() string {
return fmt.Sprintf("[%d, %d]", ts.Cpython, ts.Cgo)
}
// getTestTracer returns a Tracer with a DummyTransport
func getTestTracer() (*tracer.Tracer, *dummyTransport) {
transport := &dummyTransport{}
tracer := tracer.NewTracerTransport(transport)
return tracer, transport
}
// dummyTransport is a transport that just buffers spans and encoding
type dummyTransport struct {
traces [][]*tracer.Span
services map[string]tracer.Service
}
func (t *dummyTransport) SendTraces(traces [][]*tracer.Span) (*http.Response, error) {
t.traces = append(t.traces, traces...)
return nil, nil
}
func (t *dummyTransport) SendServices(services map[string]tracer.Service) (*http.Response, error) {
t.services = services
return nil, nil
}
func (t *dummyTransport) Traces() [][]*tracer.Span {
traces := t.traces
t.traces = nil
return traces
}
func (t *dummyTransport) SetHeader(key, value string) {}

View file

@ -0,0 +1,169 @@
package sqltraced_test
import (
"context"
"log"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
)
// To trace the sql calls, you just need to open your sql.DB with OpenTraced.
// All calls through this sql.DB object will then be traced.
func Example() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltraced.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// All calls through the database/sql API will then be traced.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// If you want to link your db calls with existing traces, you need to use
// the context version of the database/sql API.
// Just make sure you are passing the parent span within the context.
func Example_context() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltraced.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// We create a parent span and put it within the context.
span := tracer.NewRootSpan("postgres.parent", "web-backend", "query-parent")
ctx := tracer.ContextWithSpan(context.Background(), span)
// We need to use the context version of the database/sql API
// in order to link this call with the parent span.
db.PingContext(ctx)
rows, _ := db.QueryContext(ctx, "SELECT * FROM city LIMIT 5")
rows.Close()
stmt, _ := db.PrepareContext(ctx, "INSERT INTO city(name) VALUES($1)")
stmt.Exec("New York")
stmt, _ = db.PrepareContext(ctx, "SELECT name FROM city LIMIT $1")
rows, _ = stmt.Query(1)
rows.Close()
stmt.Close()
tx, _ := db.BeginTx(ctx, nil)
tx.ExecContext(ctx, "INSERT INTO city(name) VALUES('New York')")
rows, _ = tx.QueryContext(ctx, "SELECT * FROM city LIMIT 5")
rows.Close()
stmt, _ = tx.PrepareContext(ctx, "SELECT name FROM city LIMIT $1")
rows, _ = stmt.Query(1)
rows.Close()
stmt.Close()
tx.Commit()
// Calling span.Finish() will send the span into the tracer's buffer
// and then being processed.
span.Finish()
}
// You can trace all drivers implementing the database/sql/driver interface.
// For example, you can trace the go-sql-driver/mysql with the following code.
func Example_mySQL() {
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
db, err := sqltraced.OpenTraced(&mysql.MySQLDriver{}, "user:password@/dbname", "web-backend")
if err != nil {
log.Fatal(err)
}
// All calls through the database/sql API will then be traced.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// OpenTraced will first register a traced version of the driver and then will return the sql.DB object
// that holds the connection with the database.
func ExampleOpenTraced() {
// The first argument is a reference to the driver to trace.
// The second argument is the dataSourceName.
// The third argument is used to specify the name of the service under which traces will appear in the Datadog app.
// The last argument allows you to specify a custom tracer to use for tracing.
db, err := sqltraced.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
if err != nil {
log.Fatal(err)
}
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// You can use a custom tracer by passing it through the optional last argument of OpenTraced.
func ExampleOpenTraced_tracer() {
// Create and customize a new tracer that will forward 50% of generated traces to the agent.
// (useful to manage resource usage in high-throughput environments)
trc := tracer.NewTracer()
trc.SetSampleRate(0.5)
// Pass your custom tracer through the last argument of OpenTraced to trace your db calls with it.
db, err := sqltraced.OpenTraced(&pq.Driver{}, "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend", trc)
if err != nil {
log.Fatal(err)
}
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// If you need more granularity, you can register the traced driver seperately from the Open call.
func ExampleRegister() {
// Register a traced version of your driver.
sqltraced.Register("postgres", &pq.Driver{})
// Returns a sql.DB object that holds the traced connection to the database.
// Note: the sql.DB object returned by sql.Open will not be traced so make sure to use sqltraced.Open.
db, _ := sqltraced.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
defer db.Close()
// Use the database/sql API as usual and see traces appear in the Datadog app.
rows, err := db.Query("SELECT name FROM users WHERE age=?", 27)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
}
// You can use a custom tracer by passing it through the optional last argument of Register.
func ExampleRegister_tracer() {
// Create and customize a new tracer that will forward 50% of generated traces to the agent.
// (useful to manage resource usage in high-throughput environments)
trc := tracer.NewTracer()
trc.SetSampleRate(0.5)
// Register a traced version of your driver and specify to use the previous tracer
// to send the traces to the agent.
sqltraced.Register("postgres", &pq.Driver{}, trc)
// Returns a sql.DB object that holds the traced connection to the database.
// Note: the sql.DB object returned by sql.Open will not be traced so make sure to use sqltraced.Open.
db, _ := sqltraced.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable", "web-backend")
defer db.Close()
}

View file

@ -0,0 +1,41 @@
package sqltraced
import (
"log"
"testing"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/sqltest"
"github.com/DataDog/dd-trace-go/tracer/tracertest"
"github.com/go-sql-driver/mysql"
)
func TestMySQL(t *testing.T) {
trc, transport := tracertest.GetTestTracer()
db, err := OpenTraced(&mysql.MySQLDriver{}, "test:test@tcp(127.0.0.1:53306)/test", "mysql-test", trc)
if err != nil {
log.Fatal(err)
}
defer db.Close()
testDB := &sqltest.DB{
DB: db,
Tracer: trc,
Transport: transport,
DriverName: "mysql",
}
expectedSpan := &tracer.Span{
Name: "mysql.query",
Service: "mysql-test",
Type: "sql",
}
expectedSpan.Meta = map[string]string{
"db.user": "test",
"out.host": "127.0.0.1",
"out.port": "53306",
"db.name": "test",
}
sqltest.AllSQLTests(t, testDB, expectedSpan)
}

View file

@ -0,0 +1,42 @@
package sqltraced
import (
"github.com/DataDog/dd-trace-go/tracer/contrib/sqltraced/parsedsn"
)
// parseDSN returns all information passed through the DSN:
func parseDSN(driverName, dsn string) (meta map[string]string, err error) {
switch driverName {
case "mysql":
meta, err = parsedsn.MySQL(dsn)
case "postgres":
meta, err = parsedsn.Postgres(dsn)
}
meta = normalize(meta)
return meta, err
}
func normalize(meta map[string]string) map[string]string {
m := make(map[string]string)
for k, v := range meta {
if nk, ok := normalizeKey(k); ok {
m[nk] = v
}
}
return m
}
func normalizeKey(k string) (string, bool) {
switch k {
case "user":
return "db.user", true
case "application_name":
return "db.application", true
case "dbname":
return "db.name", true
case "host", "port":
return "out." + k, true
default:
return "", false
}
}

View file

@ -0,0 +1,44 @@
package sqltraced
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDSN(t *testing.T) {
assert := assert.New(t)
expected := map[string]string{
"db.user": "bob",
"out.host": "1.2.3.4",
"out.port": "5432",
"db.name": "mydb",
}
m, err := parseDSN("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
expected = map[string]string{
"db.user": "bob",
"out.host": "1.2.3.4",
"out.port": "5432",
"db.name": "mydb",
}
m, err = parseDSN("mysql", "bob:secret@tcp(1.2.3.4:5432)/mydb")
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
expected = map[string]string{
"out.port": "5433",
"out.host": "master-db-master-active.postgres.service.consul",
"db.name": "dogdatastaging",
"db.application": "trace-api",
"db.user": "dog",
}
dsn := "connect_timeout=0 binary_parameters=no password=zMWmQz26GORmgVVKEbEl dbname=dogdatastaging application_name=trace-api port=5433 sslmode=disable host=master-db-master-active.postgres.service.consul user=dog"
m, err = parseDSN("postgres", dsn)
assert.Equal(nil, err)
assert.True(reflect.DeepEqual(expected, m))
}

View file

@ -0,0 +1,25 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const defaultCollation = "utf8_general_ci"
// A blacklist of collations which is unsafe to interpolate parameters.
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
var unsafeCollations = map[string]bool{
"big5_chinese_ci": true,
"sjis_japanese_ci": true,
"gbk_chinese_ci": true,
"big5_bin": true,
"gb2312_bin": true,
"gbk_bin": true,
"sjis_bin": true,
"cp932_japanese_ci": true,
"cp932_bin": true,
}

View file

@ -0,0 +1,148 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"crypto/tls"
"errors"
"strings"
"time"
)
var (
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
)
// Config is a configuration parsed from a DSN string
type Config struct {
User string // Username
Passwd string // Password (requires User)
Net string // Network type
Addr string // Network address (requires Net)
DBName string // Database name
Params map[string]string // Connection parameters
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
TLSConfig string // TLS configuration name
tls *tls.Config // TLS configuration
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
AllowCleartextPasswords bool // Allows the cleartext client side plugin
AllowNativePasswords bool // Allows the native password authentication method
AllowOldPasswords bool // Allows the old insecure password method
ClientFoundRows bool // Return number of matching rows instead of rows changed
ColumnsWithAlias bool // Prepend table alias to column names
InterpolateParams bool // Interpolate placeholders into query string
MultiStatements bool // Allow multiple statements in one query
ParseTime bool // Parse time values to time.Time
Strict bool // Return warnings as errors
}
// ParseDSN parses the DSN string to a Config
func ParseDSN(dsn string) (cfg *Config, err error) {
// New config with some default values
cfg = &Config{
Loc: time.UTC,
Collation: defaultCollation,
}
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find the last '/' (since the password or the net addr might contain a '/')
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] == '/' {
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// [username[:password]@][protocol[(address)]]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the first ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
cfg.Passwd = dsn[k+1 : j]
break
}
}
cfg.User = dsn[:k]
break
}
}
// [protocol[(address)]]
// Find the first '(' in dsn[j+1:i]
for k = j + 1; k < i; k++ {
if dsn[k] == '(' {
// dsn[i-1] must be == ')' if an address is specified
if dsn[i-1] != ')' {
if strings.ContainsRune(dsn[k+1:i], ')') {
return nil, errInvalidDSNUnescaped
}
return nil, errInvalidDSNAddr
}
cfg.Addr = dsn[k+1 : i-1]
break
}
}
cfg.Net = dsn[j+1 : k]
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
break
}
}
cfg.DBName = dsn[i+1 : j]
break
}
}
if !foundSlash && len(dsn) > 0 {
return nil, errInvalidDSNNoSlash
}
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
return nil, errInvalidDSNUnsafeCollation
}
// Set default network if empty
if cfg.Net == "" {
cfg.Net = "tcp"
}
// Set default address if empty
if cfg.Addr == "" {
switch cfg.Net {
case "tcp":
cfg.Addr = "127.0.0.1:3306"
case "unix":
cfg.Addr = "/tmp/mysql.sock"
default:
return nil, errors.New("default addr for network '" + cfg.Net + "' unknown")
}
}
return
}

View file

@ -0,0 +1,3 @@
// Package mysql is the minimal fork of go-sql-driver/mysql so we can use their code
// to parse the mysql DSNs
package mysql

Some files were not shown because too many files have changed in this diff Show more