Commit 87790dd4 authored by Yamil Asusta's avatar Yamil Asusta Committed by John Belamaric

Add DD support (#1596)

parent 95342dfa
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/DataDog/dd-trace-go"
packages = [
"opentracing",
"tracer",
"tracer/ext"
]
revision = "27617015d45e6cd550b9a7ac7715c37cc2f7d020"
version = "v0.6.1"
[[projects]] [[projects]]
name = "github.com/PuerkitoBio/purell" name = "github.com/PuerkitoBio/purell"
packages = ["."] packages = ["."]
...@@ -378,8 +388,7 @@ ...@@ -378,8 +388,7 @@
[[projects]] [[projects]]
name = "github.com/ugorji/go" name = "github.com/ugorji/go"
packages = ["codec"] packages = ["codec"]
revision = "9831f2c3ac1068a78f50999a30db84270f647af6" revision = "9c7f9b7a2bc3a520f7c7b30b34b7f85f47fe27b6"
version = "v1.1"
[[projects]] [[projects]]
name = "github.com/v2pro/plz" name = "github.com/v2pro/plz"
...@@ -596,6 +605,6 @@ ...@@ -596,6 +605,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "e302488e45c3f12eae09558fb7c71cf495826866cb5b764745c10fffcb325537" inputs-digest = "34ebe11afea36c4e0094c66da89498960a8bc8fadea034bffb24bcb935cdb37e"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1
...@@ -16,8 +16,8 @@ The simplest form is just: ...@@ -16,8 +16,8 @@ The simplest form is just:
trace [ENDPOINT-TYPE] [ENDPOINT] trace [ENDPOINT-TYPE] [ENDPOINT]
~~~ ~~~
* **ENDPOINT-TYPE** is the type of tracing destination. Currently only `zipkin` is supported * **ENDPOINT-TYPE** is the type of tracing destination. Currently only `zipkin` and `datadog` are supported.
and that is what it defaults to. Defaults to `zipkin`.
* **ENDPOINT** is the tracing destination, and defaults to `localhost:9411`. For Zipkin, if * **ENDPOINT** is the tracing destination, and defaults to `localhost:9411`. For Zipkin, if
ENDPOINT does not begin with `http`, then it will be transformed to `http://ENDPOINT/api/v1/spans`. ENDPOINT does not begin with `http`, then it will be transformed to `http://ENDPOINT/api/v1/spans`.
...@@ -69,6 +69,12 @@ the standard Zipkin URL you can do something like: ...@@ -69,6 +69,12 @@ the standard Zipkin URL you can do something like:
trace http://tracinghost:9411/zipkin/api/v1/spans trace http://tracinghost:9411/zipkin/api/v1/spans
~~~ ~~~
Using DataDog:
~~~
trace datadog localhost:8125
~~~
Trace one query every 10000 queries, rename the service, and enable same span: Trace one query every 10000 queries, rename the service, and enable same span:
~~~ ~~~
......
...@@ -36,7 +36,7 @@ func setup(c *caddy.Controller) error { ...@@ -36,7 +36,7 @@ func setup(c *caddy.Controller) error {
func traceParse(c *caddy.Controller) (*trace, error) { func traceParse(c *caddy.Controller) (*trace, error) {
var ( var (
tr = &trace{Endpoint: defEP, EndpointType: defEpType, every: 1, serviceName: defServiceName} tr = &trace{every: 1, serviceName: defServiceName}
err error err error
) )
...@@ -47,12 +47,12 @@ func traceParse(c *caddy.Controller) (*trace, error) { ...@@ -47,12 +47,12 @@ func traceParse(c *caddy.Controller) (*trace, error) {
args := c.RemainingArgs() args := c.RemainingArgs()
switch len(args) { switch len(args) {
case 0: case 0:
tr.Endpoint, err = normalizeEndpoint(tr.EndpointType, defEP) tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(defEpType, "")
case 1: case 1:
tr.Endpoint, err = normalizeEndpoint(defEpType, args[0]) tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(defEpType, args[0])
case 2: case 2:
tr.EndpointType = strings.ToLower(args[0]) epType := strings.ToLower(args[0])
tr.Endpoint, err = normalizeEndpoint(tr.EndpointType, args[1]) tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(epType, args[1])
default: default:
err = c.ArgErr() err = c.ArgErr()
} }
...@@ -94,20 +94,30 @@ func traceParse(c *caddy.Controller) (*trace, error) { ...@@ -94,20 +94,30 @@ func traceParse(c *caddy.Controller) (*trace, error) {
return tr, err return tr, err
} }
func normalizeEndpoint(epType, ep string) (string, error) { func normalizeEndpoint(epType, ep string) (string, string, error) {
switch epType { if _, ok := supportedProviders[epType]; !ok {
case "zipkin": return "", "", fmt.Errorf("tracing endpoint type '%s' is not supported", epType)
}
if ep == "" {
ep = supportedProviders[epType]
}
if epType == "zipkin" {
if !strings.Contains(ep, "http") { if !strings.Contains(ep, "http") {
ep = "http://" + ep + "/api/v1/spans" ep = "http://" + ep + "/api/v1/spans"
} }
return ep, nil
default:
return "", fmt.Errorf("tracing endpoint type '%s' is not supported", epType)
} }
return epType, ep, nil
}
var supportedProviders = map[string]string{
"zipkin": "localhost:9411",
"datadog": "localhost:8126",
} }
const ( const (
defEP = "localhost:9411"
defEpType = "zipkin" defEpType = "zipkin"
defServiceName = "coredns" defServiceName = "coredns"
) )
...@@ -20,7 +20,8 @@ func TestTraceParse(t *testing.T) { ...@@ -20,7 +20,8 @@ func TestTraceParse(t *testing.T) {
{`trace localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false}, {`trace localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false},
{`trace http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false}, {`trace http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false},
{`trace zipkin localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false}, {`trace zipkin localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false},
{`trace zipkin http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false}, {`trace datadog localhost`, false, "localhost", 1, `coredns`, false},
{`trace datadog http://localhost:8127`, false, "http://localhost:8127", 1, `coredns`, false},
{"trace {\n every 100\n}", false, "http://localhost:9411/api/v1/spans", 100, `coredns`, false}, {"trace {\n every 100\n}", false, "http://localhost:9411/api/v1/spans", 100, `coredns`, false},
{"trace {\n every 100\n service foobar\nclient_server\n}", false, "http://localhost:9411/api/v1/spans", 100, `foobar`, true}, {"trace {\n every 100\n service foobar\nclient_server\n}", false, "http://localhost:9411/api/v1/spans", 100, `foobar`, true},
{"trace {\n every 2\n client_server true\n}", false, "http://localhost:9411/api/v1/spans", 2, `coredns`, true}, {"trace {\n every 2\n client_server true\n}", false, "http://localhost:9411/api/v1/spans", 2, `coredns`, true},
......
...@@ -3,6 +3,7 @@ package trace ...@@ -3,6 +3,7 @@ package trace
import ( import (
"fmt" "fmt"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
...@@ -10,6 +11,7 @@ import ( ...@@ -10,6 +11,7 @@ import (
// Plugin the trace package. // Plugin the trace package.
_ "github.com/coredns/coredns/plugin/pkg/trace" _ "github.com/coredns/coredns/plugin/pkg/trace"
ddtrace "github.com/DataDog/dd-trace-go/opentracing"
"github.com/miekg/dns" "github.com/miekg/dns"
ot "github.com/opentracing/opentracing-go" ot "github.com/opentracing/opentracing-go"
zipkin "github.com/openzipkin/zipkin-go-opentracing" zipkin "github.com/openzipkin/zipkin-go-opentracing"
...@@ -40,6 +42,8 @@ func (t *trace) OnStartup() error { ...@@ -40,6 +42,8 @@ func (t *trace) OnStartup() error {
switch t.EndpointType { switch t.EndpointType {
case "zipkin": case "zipkin":
err = t.setupZipkin() err = t.setupZipkin()
case "datadog":
err = t.setupDatadog()
default: default:
err = fmt.Errorf("unknown endpoint type: %s", t.EndpointType) err = fmt.Errorf("unknown endpoint type: %s", t.EndpointType)
} }
...@@ -60,6 +64,22 @@ func (t *trace) setupZipkin() error { ...@@ -60,6 +64,22 @@ func (t *trace) setupZipkin() error {
return err return err
} }
func (t *trace) setupDatadog() error {
config := ddtrace.NewConfiguration()
config.ServiceName = t.serviceName
host := strings.Split(t.Endpoint, ":")
config.AgentHostname = host[0]
if len(host) == 2 {
config.AgentPort = host[1]
}
tracer, _, err := ddtrace.NewTracer(config)
t.tracer = tracer
return err
}
// Name implements the Handler interface. // Name implements the Handler interface.
func (t *trace) Name() string { func (t *trace) Name() string {
return "trace" return "trace"
......
# go
bin/
# profiling
*.test
*.out
# generic
.DS_Store
*.cov
*.lock
*.swp
# Gopkg.toml:
# this `dep` file is used only to lock Tracer dependencies. It's not meant to be
# used by end users so no integrations dependencies must be added here. If you update
# or add a new dependency, remember to commit the `vendor` folder. To prepare
# your development environment, remember to use `rake init` instead.
# ignore integrations dependencies
ignored = [
"github.com/opentracing/*",
"github.com/cihub/seelog",
"github.com/gin-gonic/gin",
"github.com/go-redis/redis",
"github.com/go-sql-driver/mysql",
"github.com/gocql/gocql",
"github.com/gorilla/mux",
"github.com/jmoiron/sqlx",
"github.com/lib/pq",
"google.golang.org/grpc",
"gopkg.in/olivere/elastic.v3",
"gopkg.in/olivere/elastic.v5",
"github.com/stretchr/*",
"github.com/garyburd/*",
"github.com/golang/*",
"google.golang.org/*",
"golang.org/x/*",
]
[[constraint]]
name = "github.com/ugorji/go"
revision = "9c7f9b7a2bc3a520f7c7b30b34b7f85f47fe27b6"
Copyright (c) 2016, Datadog <info@datadoghq.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Datadog nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Component,Origin,License,Copyright
import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors
\ No newline at end of file
[![CircleCI](https://circleci.com/gh/DataDog/dd-trace-go/tree/master.svg?style=svg)](https://circleci.com/gh/DataDog/dd-trace-go/tree/master)
[![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/DataDog/dd-trace-go/opentracing)
Datadog APM client that implements an [OpenTracing](http://opentracing.io) Tracer.
## Initialization
To start using the Datadog Tracer with the OpenTracing API, you should first initialize the tracer with a proper `Configuration` object:
```go
import (
// ddtrace namespace is suggested
ddtrace "github.com/DataDog/dd-trace-go/opentracing"
opentracing "github.com/opentracing/opentracing-go"
)
func main() {
// create a Tracer configuration
config := ddtrace.NewConfiguration()
config.ServiceName = "api-intake"
config.AgentHostname = "ddagent.consul.local"
// initialize a Tracer and ensure a graceful shutdown
// using the `closer.Close()`
tracer, closer, err := ddtrace.NewTracer(config)
if err != nil {
// handle the configuration error
}
defer closer.Close()
// set the Datadog tracer as a GlobalTracer
opentracing.SetGlobalTracer(tracer)
startWebServer()
}
```
Function `NewTracer(config)` returns an `io.Closer` instance that can be used to gracefully shutdown the `tracer`. It's recommended to always call the `closer.Close()`, otherwise internal buffers are not flushed and you may lose some traces.
## Usage
See [Opentracing documentation](https://github.com/opentracing/opentracing-go) for some usage patterns. Legacy documentation is available in [GoDoc format](https://godoc.org/github.com/DataDog/dd-trace-go/tracer).
## Contributing Quick Start
Requirements:
* Go 1.7 or later
* Docker
* Rake
* [gometalinter](https://github.com/alecthomas/gometalinter)
### Run the tests
Start the containers defined in `docker-compose.yml` so that integrations can be tested:
```
$ docker-compose up -d
$ ./wait-for-services.sh # wait that all services are up and running
```
Fetch package's third-party dependencies (integrations and testing utilities):
```
$ rake init
```
This will only work if your working directory is in $GOPATH/src.
Now, you can run your tests via :
```
$ rake test:lint # linting via gometalinter
$ rake test:all # test the tracer and all integrations
$ rake test:race # use the -race flag
```
## Further Reading
Automatically traced libraries and frameworks: https://godoc.org/github.com/DataDog/dd-trace-go/tracer#pkg-subdirectories
Sample code: https://godoc.org/github.com/DataDog/dd-trace-go/tracer#pkg-examples
require_relative 'tasks/common'
require_relative 'tasks/vendors'
require_relative 'tasks/testing'
require_relative 'tasks/benchmarks'
machine:
services:
- docker
environment:
GODIST: "go1.9.linux-amd64.tar.gz"
IMPORT_PATH: "/home/ubuntu/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
post:
- mkdir -p download
- test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
- sudo rm -rf /usr/local/go
- sudo tar -C /usr/local -xzf download/$GODIST
dependencies:
pre:
# clean the workspace
- rm -Rf /home/ubuntu/.go_workspace/src/*
# we should use an old docker-compose because CircleCI supports
# only docker-engine==1.9
- pip install docker-compose==1.7.1
override:
# put the package in the right $GOPATH
- mkdir -p "$IMPORT_PATH"
- rsync -azr --delete ./ "$IMPORT_PATH"
- cd "$IMPORT_PATH" && rake init
test:
override:
# run the agent and backing services
- docker-compose up -d | cat
# wait for external services and execute tests
- cd "$IMPORT_PATH" && ./wait-for-services.sh
- cd "$IMPORT_PATH" && rake test:lint
- cd "$IMPORT_PATH" && rake test:all
- cd "$IMPORT_PATH" && rake test:race
- cd "$IMPORT_PATH" && rake test:coverage
post:
# add the coverage HTML report as CircleCI artifact
- cd "$IMPORT_PATH" && go tool cover -html=code.cov -o $CIRCLE_ARTIFACTS/coverage.html
# 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.
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()
}
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)
}
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
}
}
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))
}
// 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,
}
// 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
}
// Package mysql is the minimal fork of go-sql-driver/mysql so we can use their code
// to parse the mysql DSNs
package mysql
// 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
}
// 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
}
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))
}
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
}
// Package pq is the minimal fork of lib/pq so we can use their code to parse the postgres DSNs
package pq
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
}
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)
}
// 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)
}
// 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
}
// Package sqlutils share some utils functions for sql packages
package sqlutils
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
}
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))
}
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
}
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)
}
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")
}
// 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...)
}
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) {}
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!")
})
}
// 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)
}
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) {}
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()
}
// 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
}
}
}
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) {}
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()
}
// 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
}
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) {}
#!/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
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;
}
// 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,
}
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
}
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) {}
#!/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
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;
}
// 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,
}
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
}
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) {}
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)
}
// 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
}
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()
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Package opentracing implements an OpenTracing (http://opentracing.io)
// compatible tracer. A Datadog tracer must be initialized through
// a Configuration object as you can see in the following examples.
package opentracing
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment