Add DD support (#1596)

This commit is contained in:
Yamil Asusta 2018-03-09 16:08:57 -04:00 committed by John Belamaric
parent 95342dfaad
commit 87790dd47c
214 changed files with 69817 additions and 18 deletions

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

@ -0,0 +1,12 @@
# go
bin/
# profiling
*.test
*.out
# generic
.DS_Store
*.cov
*.lock
*.swp

30
vendor/github.com/DataDog/dd-trace-go/Gopkg.toml generated vendored Normal file
View file

@ -0,0 +1,30 @@
# 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"

24
vendor/github.com/DataDog/dd-trace-go/LICENSE generated vendored Normal file
View file

@ -0,0 +1,24 @@
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.

View file

@ -0,0 +1,2 @@
Component,Origin,License,Copyright
import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors
1 Component Origin License Copyright
2 import io.opentracing Apache-2.0 Copyright 2016-2017 The OpenTracing Authors

80
vendor/github.com/DataDog/dd-trace-go/README.md generated vendored Normal file
View file

@ -0,0 +1,80 @@
[![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

4
vendor/github.com/DataDog/dd-trace-go/Rakefile generated vendored Normal file
View file

@ -0,0 +1,4 @@
require_relative 'tasks/common'
require_relative 'tasks/vendors'
require_relative 'tasks/testing'
require_relative 'tasks/benchmarks'

40
vendor/github.com/DataDog/dd-trace-go/circle.yml generated vendored Normal file
View file

@ -0,0 +1,40 @@
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,131 @@
// Package redigo provides tracing for the Redigo Redis client (https://github.com/garyburd/redigo)
package redigo
import (
"bytes"
"context"
"fmt"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
redis "github.com/garyburd/redigo/redis"
"net"
"net/url"
"strconv"
"strings"
)
// TracedConn is an implementation of the redis.Conn interface that supports tracing
type TracedConn struct {
redis.Conn
p traceParams
}
// traceParams contains fields and metadata useful for command tracing
type traceParams struct {
tracer *tracer.Tracer
service string
network string
host string
port string
}
// TracedDial takes a Conn returned by redis.Dial and configures it to emit spans with the given service name
func TracedDial(service string, tracer *tracer.Tracer, network, address string, options ...redis.DialOption) (redis.Conn, error) {
c, err := redis.Dial(network, address, options...)
addr := strings.Split(address, ":")
var host, port string
if len(addr) == 2 && addr[1] != "" {
port = addr[1]
} else {
port = "6379"
}
host = addr[0]
tracer.SetServiceInfo(service, "redis", ext.AppTypeDB)
tc := TracedConn{c, traceParams{tracer, service, network, host, port}}
return tc, err
}
// TracedDialURL takes a Conn returned by redis.DialURL and configures it to emit spans with the given service name
func TracedDialURL(service string, tracer *tracer.Tracer, rawurl string, options ...redis.DialOption) (redis.Conn, error) {
u, err := url.Parse(rawurl)
if err != nil {
return TracedConn{}, err
}
// Getting host and port, usind code from https://github.com/garyburd/redigo/blob/master/redis/conn.go#L226
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Host
port = "6379"
}
if host == "" {
host = "localhost"
}
// Set in redis.DialUrl source code
network := "tcp"
c, err := redis.DialURL(rawurl, options...)
tc := TracedConn{c, traceParams{tracer, service, network, host, port}}
return tc, err
}
// NewChildSpan creates a span inheriting from the given context. It adds to the span useful metadata about the traced Redis connection
func (tc TracedConn) NewChildSpan(ctx context.Context) *tracer.Span {
span := tc.p.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = tc.p.service
span.SetMeta("out.network", tc.p.network)
span.SetMeta("out.port", tc.p.port)
span.SetMeta("out.host", tc.p.host)
return span
}
// Do wraps redis.Conn.Do. It sends a command to the Redis server and returns the received reply.
// In the process it emits a span containing key information about the command sent.
// When passed a context.Context as the final argument, Do will ensure that any span created
// inherits from this context. The rest of the arguments are passed through to the Redis server unchanged
func (tc TracedConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
var ctx context.Context
var ok bool
if len(args) > 0 {
ctx, ok = args[len(args)-1].(context.Context)
if ok {
args = args[:len(args)-1]
}
}
span := tc.NewChildSpan(ctx)
defer func() {
if err != nil {
span.SetError(err)
}
span.Finish()
}()
span.SetMeta("redis.args_length", strconv.Itoa(len(args)))
if len(commandName) > 0 {
span.Resource = commandName
} else {
// When the command argument to the Do method is "", then the Do method will flush the output buffer
// See https://godoc.org/github.com/garyburd/redigo/redis#hdr-Pipelining
span.Resource = "redigo.Conn.Flush"
}
var b bytes.Buffer
b.WriteString(commandName)
for _, arg := range args {
b.WriteString(" ")
switch arg := arg.(type) {
case string:
b.WriteString(arg)
case int:
b.WriteString(strconv.Itoa(arg))
case int32:
b.WriteString(strconv.FormatInt(int64(arg), 10))
case int64:
b.WriteString(strconv.FormatInt(arg, 10))
case fmt.Stringer:
b.WriteString(arg.String())
}
}
span.SetMeta("redis.raw_command", b.String())
return tc.Conn.Do(commandName, args...)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,81 @@
package redis_test
import (
"context"
"fmt"
redistrace "github.com/DataDog/dd-trace-go/contrib/go-redis/redis"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/contrib/gin-gonic/gintrace"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"time"
)
// To start tracing Redis commands, use the NewTracedClient function to create a traced Redis clienty,
// passing in a service name of choice.
func Example() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := redistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// Emit spans per command by using your Redis connection as usual
c.Set("test_key", "test_value", 0)
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When set with a context, the traced client will emit a span inheriting from 'parent.request'
c.SetContext(ctx)
c.Set("food", "cheese", 0)
root.Finish()
// Contexts can be easily passed between Datadog integrations
r := gin.Default()
r.Use(gintrace.Middleware("web-admin"))
client := redistrace.NewTracedClient(opts, tracer.DefaultTracer, "redis-img-backend")
r.GET("/user/settings/:id", func(ctx *gin.Context) {
// create a span that is a child of your http request
client.SetContext(ctx)
client.Get(fmt.Sprintf("cached_user_details_%s", ctx.Param("id")))
})
}
// You can also trace Redis Pipelines
func Example_pipeline() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := redistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// pipe is a TracedPipeliner
pipe := c.Pipeline()
pipe.Incr("pipeline_counter")
pipe.Expire("pipeline_counter", time.Hour)
pipe.Exec()
}
func ExampleNewTracedClient() {
opts := &redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default db
}
c := redistrace.NewTracedClient(opts, tracer.DefaultTracer, "my-redis-backend")
// Emit spans per command by using your Redis connection as usual
c.Set("test_key", "test_value", 0)
// Use a context to pass information down the call chain
root := tracer.NewRootSpan("parent.request", "web", "/home")
ctx := root.Context(context.Background())
// When set with a context, the traced client will emit a span inheriting from 'parent.request'
c.SetContext(ctx)
c.Set("food", "cheese", 0)
root.Finish()
}

View file

@ -0,0 +1,151 @@
// Package redis provides tracing for the go-redis Redis client (https://github.com/go-redis/redis)
package redis
import (
"bytes"
"context"
"github.com/DataDog/dd-trace-go/tracer"
"github.com/DataDog/dd-trace-go/tracer/ext"
"github.com/go-redis/redis"
"strconv"
"strings"
)
// TracedClient is used to trace requests to a redis server.
type TracedClient struct {
*redis.Client
traceParams traceParams
}
// TracedPipeline is used to trace pipelines executed on a redis server.
type TracedPipeliner struct {
redis.Pipeliner
traceParams traceParams
}
type traceParams struct {
host string
port string
db string
service string
tracer *tracer.Tracer
}
// NewTracedClient takes a Client returned by redis.NewClient and configures it to emit spans under the given service name
func NewTracedClient(opt *redis.Options, t *tracer.Tracer, service string) *TracedClient {
var host, port string
addr := strings.Split(opt.Addr, ":")
if len(addr) == 2 && addr[1] != "" {
port = addr[1]
} else {
port = "6379"
}
host = addr[0]
db := strconv.Itoa(opt.DB)
client := redis.NewClient(opt)
t.SetServiceInfo(service, "redis", ext.AppTypeDB)
tc := &TracedClient{
client,
traceParams{
host,
port,
db,
service,
t},
}
tc.Client.WrapProcess(createWrapperFromClient(tc))
return tc
}
// Pipeline creates a TracedPipeline from a TracedClient
func (c *TracedClient) Pipeline() *TracedPipeliner {
return &TracedPipeliner{
c.Client.Pipeline(),
c.traceParams,
}
}
// ExecWithContext calls Pipeline.Exec(). It ensures that the resulting Redis calls
// are traced, and that emitted spans are children of the given Context
func (c *TracedPipeliner) ExecWithContext(ctx context.Context) ([]redis.Cmder, error) {
span := c.traceParams.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = c.traceParams.service
span.SetMeta("out.host", c.traceParams.host)
span.SetMeta("out.port", c.traceParams.port)
span.SetMeta("out.db", c.traceParams.db)
cmds, err := c.Pipeliner.Exec()
if err != nil {
span.SetError(err)
}
span.Resource = String(cmds)
span.SetMeta("redis.pipeline_length", strconv.Itoa(len(cmds)))
span.Finish()
return cmds, err
}
// Exec calls Pipeline.Exec() ensuring that the resulting Redis calls are traced
func (c *TracedPipeliner) Exec() ([]redis.Cmder, error) {
span := c.traceParams.tracer.NewRootSpan("redis.command", c.traceParams.service, "redis")
span.SetMeta("out.host", c.traceParams.host)
span.SetMeta("out.port", c.traceParams.port)
span.SetMeta("out.db", c.traceParams.db)
cmds, err := c.Pipeliner.Exec()
if err != nil {
span.SetError(err)
}
span.Resource = String(cmds)
span.SetMeta("redis.pipeline_length", strconv.Itoa(len(cmds)))
span.Finish()
return cmds, err
}
// String returns a string representation of a slice of redis Commands, separated by newlines
func String(cmds []redis.Cmder) string {
var b bytes.Buffer
for _, cmd := range cmds {
b.WriteString(cmd.String())
b.WriteString("\n")
}
return b.String()
}
// SetContext sets a context on a TracedClient. Use it to ensure that emitted spans have the correct parent
func (c *TracedClient) SetContext(ctx context.Context) {
c.Client = c.Client.WithContext(ctx)
}
// createWrapperFromClient wraps tracing into redis.Process().
func createWrapperFromClient(tc *TracedClient) func(oldProcess func(cmd redis.Cmder) error) func(cmd redis.Cmder) error {
return func(oldProcess func(cmd redis.Cmder) error) func(cmd redis.Cmder) error {
return func(cmd redis.Cmder) error {
ctx := tc.Client.Context()
var resource string
resource = strings.Split(cmd.String(), " ")[0]
args_length := len(strings.Split(cmd.String(), " ")) - 1
span := tc.traceParams.tracer.NewChildSpanFromContext("redis.command", ctx)
span.Service = tc.traceParams.service
span.Resource = resource
span.SetMeta("redis.raw_command", cmd.String())
span.SetMeta("redis.args_length", strconv.Itoa(args_length))
span.SetMeta("out.host", tc.traceParams.host)
span.SetMeta("out.port", tc.traceParams.port)
span.SetMeta("out.db", tc.traceParams.db)
err := oldProcess(cmd)
if err != nil {
span.SetError(err)
}
span.Finish()
return err
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,43 @@
# File for development/ testing purposes
cassandra:
image: cassandra:3.7
ports:
- "127.0.0.1:${TEST_CASSANDRA_PORT}:9042"
mysql:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=$TEST_MYSQL_ROOT_PASSWORD
- MYSQL_PASSWORD=$TEST_MYSQL_PASSWORD
- MYSQL_USER=$TEST_MYSQL_USER
- MYSQL_DATABASE=$TEST_MYSQL_DATABASE
ports:
- "127.0.0.1:${TEST_MYSQL_PORT}:3306"
postgres:
image: postgres:9.5
environment:
- POSTGRES_PASSWORD=$TEST_POSTGRES_PASSWORD
- POSTGRES_USER=$TEST_POSTGRES_USER
- POSTGRES_DB=$TEST_POSTGRES_DB
ports:
- "127.0.0.1:${TEST_POSTGRES_PORT}:5432"
redis:
image: redis:3.2
ports:
- "127.0.0.1:${TEST_REDIS_PORT}:6379"
elasticsearch-v5:
image: elasticsearch:5
ports:
- "127.0.0.1:${TEST_ELASTICSEARCH5_PORT}:9200"
elasticsearch-v2:
image: elasticsearch:2
ports:
- "127.0.0.1:${TEST_ELASTICSEARCH2_PORT}:9200"
ddagent:
image: datadog/docker-dd-agent
environment:
- DD_APM_ENABLED=true
- DD_BIND_HOST=0.0.0.0
- DD_API_KEY=invalid_key_but_this_is_fine
ports:
- "127.0.0.1:8126:8126"

View file

@ -0,0 +1,58 @@
package opentracing
import (
"os"
"path/filepath"
)
// Configuration struct configures the Datadog tracer. Please use the NewConfiguration
// constructor to begin.
type Configuration struct {
// Enabled, when false, returns a no-op implementation of the Tracer.
Enabled bool
// Debug, when true, writes details to logs.
Debug bool
// ServiceName specifies the name of this application.
ServiceName string
// SampleRate sets the Tracer sample rate (ext/priority.go).
SampleRate float64
// AgentHostname specifies the hostname of the agent where the traces
// are sent to.
AgentHostname string
// AgentPort specifies the port that the agent is listening on.
AgentPort string
// GlobalTags holds a set of tags that will be automatically applied to
// all spans.
GlobalTags map[string]interface{}
// TextMapPropagator is an injector used for Context propagation.
TextMapPropagator Propagator
}
// NewConfiguration creates a `Configuration` object with default values.
func NewConfiguration() *Configuration {
// default service name is the Go binary name
binaryName := filepath.Base(os.Args[0])
// Configuration struct with default values
return &Configuration{
Enabled: true,
Debug: false,
ServiceName: binaryName,
SampleRate: 1,
AgentHostname: "localhost",
AgentPort: "8126",
GlobalTags: make(map[string]interface{}),
TextMapPropagator: NewTextMapPropagator("", "", ""),
}
}
type noopCloser struct{}
func (c *noopCloser) Close() error { return nil }

View file

@ -0,0 +1,58 @@
package opentracing
import (
"testing"
ot "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestConfigurationDefaults(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
assert.Equal(true, config.Enabled)
assert.Equal(false, config.Debug)
assert.Equal(float64(1), config.SampleRate)
assert.Equal("opentracing.test", config.ServiceName)
assert.Equal("localhost", config.AgentHostname)
assert.Equal("8126", config.AgentPort)
}
func TestConfiguration(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
config.SampleRate = 0
config.ServiceName = "api-intake"
config.AgentHostname = "ddagent.consul.local"
config.AgentPort = "58126"
tracer, closer, err := NewTracer(config)
assert.NotNil(tracer)
assert.NotNil(closer)
assert.Nil(err)
assert.Equal("api-intake", tracer.(*Tracer).config.ServiceName)
}
func TestTracerServiceName(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
config.ServiceName = ""
tracer, closer, err := NewTracer(config)
assert.Nil(tracer)
assert.Nil(closer)
assert.NotNil(err)
assert.Equal("A Datadog Tracer requires a valid `ServiceName` set", err.Error())
}
func TestDisabledTracer(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
config.Enabled = false
tracer, closer, err := NewTracer(config)
assert.IsType(&ot.NoopTracer{}, tracer)
assert.IsType(&noopCloser{}, closer)
assert.Nil(err)
}

View file

@ -0,0 +1,46 @@
package opentracing
// SpanContext represents Span state that must propagate to descendant Spans
// and across process boundaries.
type SpanContext struct {
traceID uint64
spanID uint64
parentID uint64
sampled bool
span *Span
baggage map[string]string
}
// ForeachBaggageItem grants access to all baggage items stored in the
// SpanContext
func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) {
for k, v := range c.baggage {
if !handler(k, v) {
break
}
}
}
// WithBaggageItem returns an entirely new SpanContext with the
// given key:value baggage pair set.
func (c SpanContext) WithBaggageItem(key, val string) SpanContext {
var newBaggage map[string]string
if c.baggage == nil {
newBaggage = map[string]string{key: val}
} else {
newBaggage = make(map[string]string, len(c.baggage)+1)
for k, v := range c.baggage {
newBaggage[k] = v
}
newBaggage[key] = val
}
// Use positional parameters so the compiler will help catch new fields.
return SpanContext{
traceID: c.traceID,
spanID: c.spanID,
parentID: c.parentID,
sampled: c.sampled,
span: c.span,
baggage: newBaggage,
}
}

View file

@ -0,0 +1,29 @@
package opentracing
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSpanContextBaggage(t *testing.T) {
assert := assert.New(t)
ctx := SpanContext{}
ctx = ctx.WithBaggageItem("key", "value")
assert.Equal("value", ctx.baggage["key"])
}
func TestSpanContextIterator(t *testing.T) {
assert := assert.New(t)
baggageIterator := make(map[string]string)
ctx := SpanContext{baggage: map[string]string{"key": "value"}}
ctx.ForeachBaggageItem(func(k, v string) bool {
baggageIterator[k] = v
return true
})
assert.Len(baggageIterator, 1)
assert.Equal("value", baggageIterator["key"])
}

View file

@ -0,0 +1,4 @@
// 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

View file

@ -0,0 +1,30 @@
package opentracing_test
import (
"context"
opentracing "github.com/opentracing/opentracing-go"
)
// You can leverage the Golang `Context` for intra-process propagation of
// Spans. In this example we create a root Span, so that it can be reused
// in a nested function to create a child Span.
func Example_startContext() {
// create a new root Span and return a new Context that includes
// the Span itself
ctx := context.Background()
rootSpan, ctx := opentracing.StartSpanFromContext(ctx, "web.request")
defer rootSpan.Finish()
requestHandler(ctx)
}
func requestHandler(ctx context.Context) {
// retrieve the previously set root Span
span := opentracing.SpanFromContext(ctx)
span.SetTag("resource.name", "/")
// or simply create a new child Span from the previous Context
childSpan, _ := opentracing.StartSpanFromContext(ctx, "sql.query")
defer childSpan.Finish()
}

View file

@ -0,0 +1,30 @@
package opentracing_test
import (
// ddtrace namespace is suggested
ddtrace "github.com/DataDog/dd-trace-go/opentracing"
opentracing "github.com/opentracing/opentracing-go"
)
func Example_initialization() {
// 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()
}
func startWebServer() {
// start a web server
}

View file

@ -0,0 +1,24 @@
package opentracing_test
import (
opentracing "github.com/opentracing/opentracing-go"
)
// You can use the GlobalTracer to create a root Span. If you need to create a hierarchy,
// simply use the `ChildOf` reference
func Example_startSpan() {
// use the GlobalTracer previously set
rootSpan := opentracing.StartSpan("web.request")
defer rootSpan.Finish()
// set the reference to create a hierarchy of spans
reference := opentracing.ChildOf(rootSpan.Context())
childSpan := opentracing.StartSpan("sql.query", reference)
defer childSpan.Finish()
dbQuery()
}
func dbQuery() {
// start a database query
}

View file

@ -0,0 +1,125 @@
package opentracing
import (
"strconv"
"strings"
ot "github.com/opentracing/opentracing-go"
)
// Propagator implementations should be able to inject and extract
// SpanContexts into an implementation specific carrier.
type Propagator interface {
// Inject takes the SpanContext and injects it into the carrier using
// an implementation specific method.
Inject(context ot.SpanContext, carrier interface{}) error
// Extract returns the SpanContext from the given carrier using an
// implementation specific method.
Extract(carrier interface{}) (ot.SpanContext, error)
}
const (
defaultBaggageHeaderPrefix = "ot-baggage-"
defaultTraceIDHeader = "x-datadog-trace-id"
defaultParentIDHeader = "x-datadog-parent-id"
)
// NewTextMapPropagator returns a new propagator which uses opentracing.TextMap
// to inject and extract values. The parameters specify the prefix that will
// be used to prefix baggage header keys along with the trace and parent header.
// Empty strings may be provided to use the defaults, which are: "ot-baggage-" as
// prefix for baggage headers, "x-datadog-trace-id" and "x-datadog-parent-id" for
// trace and parent ID headers.
func NewTextMapPropagator(baggagePrefix, traceHeader, parentHeader string) *TextMapPropagator {
if baggagePrefix == "" {
baggagePrefix = defaultBaggageHeaderPrefix
}
if traceHeader == "" {
traceHeader = defaultTraceIDHeader
}
if parentHeader == "" {
parentHeader = defaultParentIDHeader
}
return &TextMapPropagator{baggagePrefix, traceHeader, parentHeader}
}
// TextMapPropagator implements a propagator which uses opentracing.TextMap
// internally.
type TextMapPropagator struct {
baggagePrefix string
traceHeader string
parentHeader string
}
// Inject defines the TextMapPropagator to propagate SpanContext data
// out of the current process. The implementation propagates the
// TraceID and the current active SpanID, as well as the Span baggage.
func (p *TextMapPropagator) Inject(context ot.SpanContext, carrier interface{}) error {
ctx, ok := context.(SpanContext)
if !ok {
return ot.ErrInvalidSpanContext
}
writer, ok := carrier.(ot.TextMapWriter)
if !ok {
return ot.ErrInvalidCarrier
}
// propagate the TraceID and the current active SpanID
writer.Set(p.traceHeader, strconv.FormatUint(ctx.traceID, 10))
writer.Set(p.parentHeader, strconv.FormatUint(ctx.spanID, 10))
// propagate OpenTracing baggage
for k, v := range ctx.baggage {
writer.Set(p.baggagePrefix+k, v)
}
return nil
}
// Extract implements Propagator.
func (p *TextMapPropagator) Extract(carrier interface{}) (ot.SpanContext, error) {
reader, ok := carrier.(ot.TextMapReader)
if !ok {
return nil, ot.ErrInvalidCarrier
}
var err error
var traceID, parentID uint64
decodedBaggage := make(map[string]string)
// extract SpanContext fields
err = reader.ForeachKey(func(k, v string) error {
switch strings.ToLower(k) {
case p.traceHeader:
traceID, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return ot.ErrSpanContextCorrupted
}
case p.parentHeader:
parentID, err = strconv.ParseUint(v, 10, 64)
if err != nil {
return ot.ErrSpanContextCorrupted
}
default:
lowercaseK := strings.ToLower(k)
if strings.HasPrefix(lowercaseK, p.baggagePrefix) {
decodedBaggage[strings.TrimPrefix(lowercaseK, p.baggagePrefix)] = v
}
}
return nil
})
if err != nil {
return nil, err
}
if traceID == 0 || parentID == 0 {
return nil, ot.ErrSpanContextNotFound
}
return SpanContext{
traceID: traceID,
spanID: parentID,
baggage: decodedBaggage,
}, nil
}

View file

@ -0,0 +1,81 @@
package opentracing
import (
"net/http"
"strconv"
"testing"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestTracerPropagationDefaults(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
root := tracer.StartSpan("web.request")
ctx := root.Context()
headers := http.Header{}
// inject the SpanContext
carrier := opentracing.HTTPHeadersCarrier(headers)
err := tracer.Inject(ctx, opentracing.HTTPHeaders, carrier)
assert.Nil(err)
// retrieve the SpanContext
propagated, err := tracer.Extract(opentracing.HTTPHeaders, carrier)
assert.Nil(err)
tCtx, ok := ctx.(SpanContext)
assert.True(ok)
tPropagated, ok := propagated.(SpanContext)
assert.True(ok)
// compare if there is a Context match
assert.Equal(tCtx.traceID, tPropagated.traceID)
assert.Equal(tCtx.spanID, tPropagated.spanID)
// ensure a child can be created
child := tracer.StartSpan("db.query", opentracing.ChildOf(propagated))
tRoot, ok := root.(*Span)
assert.True(ok)
tChild, ok := child.(*Span)
assert.True(ok)
assert.NotEqual(uint64(0), tChild.Span.TraceID)
assert.NotEqual(uint64(0), tChild.Span.SpanID)
assert.Equal(tRoot.Span.SpanID, tChild.Span.ParentID)
assert.Equal(tRoot.Span.TraceID, tChild.Span.ParentID)
tid := strconv.FormatUint(tRoot.Span.TraceID, 10)
pid := strconv.FormatUint(tRoot.Span.SpanID, 10)
// hardcode header names to fail test if defaults are changed
assert.Equal(headers.Get("x-datadog-trace-id"), tid)
assert.Equal(headers.Get("x-datadog-parent-id"), pid)
}
func TestTracerTextMapPropagationHeader(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
config.TextMapPropagator = NewTextMapPropagator("bg-", "tid", "pid")
tracer, _, _ := NewTracer(config)
root := tracer.StartSpan("web.request").SetBaggageItem("item", "x").(*Span)
ctx := root.Context()
headers := http.Header{}
carrier := opentracing.HTTPHeadersCarrier(headers)
err := tracer.Inject(ctx, opentracing.HTTPHeaders, carrier)
assert.Nil(err)
tid := strconv.FormatUint(root.Span.TraceID, 10)
pid := strconv.FormatUint(root.Span.SpanID, 10)
assert.Equal(headers.Get("tid"), tid)
assert.Equal(headers.Get("pid"), pid)
assert.Equal(headers.Get("bg-item"), "x")
}

View file

@ -0,0 +1,151 @@
package opentracing
import (
"fmt"
"time"
ddtrace "github.com/DataDog/dd-trace-go/tracer"
ot "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
)
// Span represents an active, un-finished span in the OpenTracing system.
// Spans are created by the Tracer interface.
type Span struct {
*ddtrace.Span
context SpanContext
tracer *Tracer
}
// Tracer provides access to the `Tracer`` that created this Span.
func (s *Span) Tracer() ot.Tracer {
return s.tracer
}
// Context yields the SpanContext for this Span. Note that the return
// value of Context() is still valid after a call to Span.Finish(), as is
// a call to Span.Context() after a call to Span.Finish().
func (s *Span) Context() ot.SpanContext {
return s.context
}
// SetBaggageItem sets a key:value pair on this Span and its SpanContext
// that also propagates to descendants of this Span.
func (s *Span) SetBaggageItem(key, val string) ot.Span {
s.Span.Lock()
defer s.Span.Unlock()
s.context = s.context.WithBaggageItem(key, val)
return s
}
// BaggageItem gets the value for a baggage item given its key. Returns the empty string
// if the value isn't found in this Span.
func (s *Span) BaggageItem(key string) string {
s.Span.Lock()
defer s.Span.Unlock()
return s.context.baggage[key]
}
// SetTag adds a tag to the span, overwriting pre-existing values for
// the given `key`.
func (s *Span) SetTag(key string, value interface{}) ot.Span {
switch key {
case ServiceName:
s.Span.Lock()
defer s.Span.Unlock()
s.Span.Service = fmt.Sprint(value)
case ResourceName:
s.Span.Lock()
defer s.Span.Unlock()
s.Span.Resource = fmt.Sprint(value)
case SpanType:
s.Span.Lock()
defer s.Span.Unlock()
s.Span.Type = fmt.Sprint(value)
case Error:
switch v := value.(type) {
case nil:
// no error
case error:
s.Span.SetError(v)
default:
s.Span.SetError(fmt.Errorf("%v", v))
}
default:
// NOTE: locking is not required because the `SetMeta` is
// already thread-safe
s.Span.SetMeta(key, fmt.Sprint(value))
}
return s
}
// FinishWithOptions is like Finish() but with explicit control over
// timestamps and log data.
func (s *Span) FinishWithOptions(options ot.FinishOptions) {
if options.FinishTime.IsZero() {
options.FinishTime = time.Now().UTC()
}
s.Span.FinishWithTime(options.FinishTime.UnixNano())
}
// SetOperationName sets or changes the operation name.
func (s *Span) SetOperationName(operationName string) ot.Span {
s.Span.Lock()
defer s.Span.Unlock()
s.Span.Name = operationName
return s
}
// LogFields is an efficient and type-checked way to record key:value
// logging data about a Span, though the programming interface is a little
// more verbose than LogKV().
func (s *Span) LogFields(fields ...log.Field) {
// TODO: implementation missing
}
// LogKV is a concise, readable way to record key:value logging data about
// a Span, though unfortunately this also makes it less efficient and less
// type-safe than LogFields().
func (s *Span) LogKV(keyVals ...interface{}) {
// TODO: implementation missing
}
// LogEvent is deprecated: use LogFields or LogKV
func (s *Span) LogEvent(event string) {
// TODO: implementation missing
}
// LogEventWithPayload deprecated: use LogFields or LogKV
func (s *Span) LogEventWithPayload(event string, payload interface{}) {
// TODO: implementation missing
}
// Log is deprecated: use LogFields or LogKV
func (s *Span) Log(data ot.LogData) {
// TODO: implementation missing
}
// NewSpan is the OpenTracing Span constructor
func NewSpan(operationName string) *Span {
span := &ddtrace.Span{
Name: operationName,
}
otSpan := &Span{
Span: span,
context: SpanContext{
traceID: span.TraceID,
spanID: span.SpanID,
parentID: span.ParentID,
sampled: span.Sampled,
},
}
// SpanContext is propagated and used to create children
otSpan.context.span = otSpan
return otSpan
}

View file

@ -0,0 +1,129 @@
package opentracing
import (
"errors"
"testing"
"time"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestSpanBaggage(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
span.SetBaggageItem("key", "value")
assert.Equal("value", span.BaggageItem("key"))
}
func TestSpanContext(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
assert.NotNil(span.Context())
}
func TestSpanOperationName(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
span.SetOperationName("http.request")
assert.Equal("http.request", span.Span.Name)
}
func TestSpanFinish(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
span.Finish()
assert.True(span.Span.Duration > 0)
}
func TestSpanFinishWithTime(t *testing.T) {
assert := assert.New(t)
finishTime := time.Now().Add(10 * time.Second)
span := NewSpan("web.request")
span.FinishWithOptions(opentracing.FinishOptions{FinishTime: finishTime})
duration := finishTime.UnixNano() - span.Span.Start
assert.Equal(duration, span.Span.Duration)
}
func TestSpanSetTag(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
span.SetTag("component", "tracer")
assert.Equal("tracer", span.Meta["component"])
span.SetTag("tagInt", 1234)
assert.Equal("1234", span.Meta["tagInt"])
}
func TestSpanSetDatadogTags(t *testing.T) {
assert := assert.New(t)
span := NewSpan("web.request")
span.SetTag("span.type", "http")
span.SetTag("service.name", "db-cluster")
span.SetTag("resource.name", "SELECT * FROM users;")
assert.Equal("http", span.Span.Type)
assert.Equal("db-cluster", span.Span.Service)
assert.Equal("SELECT * FROM users;", span.Span.Resource)
}
func TestSpanSetErrorTag(t *testing.T) {
assert := assert.New(t)
for _, tt := range []struct {
name string // span name
val interface{} // tag value
msg string // error message
typ string // error type
}{
{
name: "error.error",
val: errors.New("some error"),
msg: "some error",
typ: "*errors.errorString",
},
{
name: "error.string",
val: "some string error",
msg: "some string error",
typ: "*errors.errorString",
},
{
name: "error.struct",
val: struct{ N int }{5},
msg: "{5}",
typ: "*errors.errorString",
},
{
name: "error.other",
val: 1,
msg: "1",
typ: "*errors.errorString",
},
{
name: "error.nil",
val: nil,
msg: "",
typ: "",
},
} {
span := NewSpan(tt.name)
span.SetTag(Error, tt.val)
assert.Equal(span.Meta["error.msg"], tt.msg)
assert.Equal(span.Meta["error.type"], tt.typ)
if tt.val != nil {
assert.NotEqual(span.Meta["error.stack"], "")
}
}
}

View file

@ -0,0 +1,12 @@
package opentracing
const (
// SpanType defines the Span type (web, db, cache)
SpanType = "span.type"
// ServiceName defines the Service name for this Span
ServiceName = "service.name"
// ResourceName defines the Resource name for the Span
ResourceName = "resource.name"
// Error defines an error.
Error = "error.error"
)

View file

@ -0,0 +1,178 @@
package opentracing
import (
"errors"
"io"
"time"
ddtrace "github.com/DataDog/dd-trace-go/tracer"
ot "github.com/opentracing/opentracing-go"
)
// Tracer is a simple, thin interface for Span creation and SpanContext
// propagation. In the current state, this Tracer is a compatibility layer
// that wraps the Datadog Tracer implementation.
type Tracer struct {
// impl is the Datadog Tracer implementation.
impl *ddtrace.Tracer
// config holds the Configuration used to create the Tracer.
config *Configuration
}
// StartSpan creates, starts, and returns a new Span with the given `operationName`
// A Span with no SpanReference options (e.g., opentracing.ChildOf() or
// opentracing.FollowsFrom()) becomes the root of its own trace.
func (t *Tracer) StartSpan(operationName string, options ...ot.StartSpanOption) ot.Span {
sso := ot.StartSpanOptions{}
for _, o := range options {
o.Apply(&sso)
}
return t.startSpanWithOptions(operationName, sso)
}
func (t *Tracer) startSpanWithOptions(operationName string, options ot.StartSpanOptions) ot.Span {
if options.StartTime.IsZero() {
options.StartTime = time.Now().UTC()
}
var context SpanContext
var hasParent bool
var parent *Span
var span *ddtrace.Span
for _, ref := range options.References {
ctx, ok := ref.ReferencedContext.(SpanContext)
if !ok {
// ignore the SpanContext since it's not valid
continue
}
// if we have parenting define it
if ref.Type == ot.ChildOfRef {
hasParent = true
context = ctx
parent = ctx.span
}
}
if parent == nil {
// create a root Span with the default service name and resource
span = t.impl.NewRootSpan(operationName, t.config.ServiceName, operationName)
if hasParent {
// the Context doesn't have a Span reference because it
// has been propagated from another process, so we set these
// values manually
span.TraceID = context.traceID
span.ParentID = context.spanID
t.impl.Sample(span)
}
} else {
// create a child Span that inherits from a parent
span = t.impl.NewChildSpan(operationName, parent.Span)
}
// create an OpenTracing compatible Span; the SpanContext has a
// back-reference that is used for parent-child hierarchy
otSpan := &Span{
Span: span,
context: SpanContext{
traceID: span.TraceID,
spanID: span.SpanID,
parentID: span.ParentID,
sampled: span.Sampled,
},
tracer: t,
}
otSpan.context.span = otSpan
// set start time
otSpan.Span.Start = options.StartTime.UnixNano()
if parent != nil {
// propagate baggage items
if l := len(parent.context.baggage); l > 0 {
otSpan.context.baggage = make(map[string]string, len(parent.context.baggage))
for k, v := range parent.context.baggage {
otSpan.context.baggage[k] = v
}
}
}
// add tags from options
for k, v := range options.Tags {
otSpan.SetTag(k, v)
}
// add global tags
for k, v := range t.config.GlobalTags {
otSpan.SetTag(k, v)
}
return otSpan
}
// Inject takes the `sm` SpanContext instance and injects it for
// propagation within `carrier`. The actual type of `carrier` depends on
// the value of `format`. Currently supported Injectors are:
// * `TextMap`
// * `HTTPHeaders`
func (t *Tracer) Inject(ctx ot.SpanContext, format interface{}, carrier interface{}) error {
switch format {
case ot.TextMap, ot.HTTPHeaders:
return t.config.TextMapPropagator.Inject(ctx, carrier)
}
return ot.ErrUnsupportedFormat
}
// Extract returns a SpanContext instance given `format` and `carrier`.
func (t *Tracer) Extract(format interface{}, carrier interface{}) (ot.SpanContext, error) {
switch format {
case ot.TextMap, ot.HTTPHeaders:
return t.config.TextMapPropagator.Extract(carrier)
}
return nil, ot.ErrUnsupportedFormat
}
// Close method implements `io.Closer` interface to graceful shutdown the Datadog
// Tracer. Note that this is a blocking operation that waits for the flushing Go
// routine.
func (t *Tracer) Close() error {
t.impl.Stop()
return nil
}
// NewTracer uses a Configuration object to initialize a Datadog Tracer.
// The initialization returns a `io.Closer` that can be used to graceful
// shutdown the tracer. If the configuration object defines a disabled
// Tracer, a no-op implementation is returned.
func NewTracer(config *Configuration) (ot.Tracer, io.Closer, error) {
if config.ServiceName == "" {
// abort initialization if a `ServiceName` is not defined
return nil, nil, errors.New("A Datadog Tracer requires a valid `ServiceName` set")
}
if config.Enabled == false {
// return a no-op implementation so Datadog provides the minimum overhead
return &ot.NoopTracer{}, &noopCloser{}, nil
}
// configure a Datadog Tracer
transport := ddtrace.NewTransport(config.AgentHostname, config.AgentPort)
tracer := &Tracer{
impl: ddtrace.NewTracerTransport(transport),
config: config,
}
tracer.impl.SetDebugLogging(config.Debug)
tracer.impl.SetSampleRate(config.SampleRate)
// set the new Datadog Tracer as a `DefaultTracer` so it can be
// used in integrations. NOTE: this is a temporary implementation
// that can be removed once all integrations have been migrated
// to the OpenTracing API.
ddtrace.DefaultTracer = tracer.impl
return tracer, tracer, nil
}

View file

@ -0,0 +1,131 @@
package opentracing
import (
"testing"
"time"
ddtrace "github.com/DataDog/dd-trace-go/tracer"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
)
func TestDefaultTracer(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
tTracer, ok := tracer.(*Tracer)
assert.True(ok)
assert.Equal(tTracer.impl, ddtrace.DefaultTracer)
}
func TestTracerStartSpan(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
span, ok := tracer.StartSpan("web.request").(*Span)
assert.True(ok)
assert.NotEqual(uint64(0), span.Span.TraceID)
assert.NotEqual(uint64(0), span.Span.SpanID)
assert.Equal(uint64(0), span.Span.ParentID)
assert.Equal("web.request", span.Span.Name)
assert.Equal("opentracing.test", span.Span.Service)
assert.NotNil(span.Span.Tracer())
}
func TestTracerStartChildSpan(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
root := tracer.StartSpan("web.request")
child := tracer.StartSpan("db.query", opentracing.ChildOf(root.Context()))
tRoot, ok := root.(*Span)
assert.True(ok)
tChild, ok := child.(*Span)
assert.True(ok)
assert.NotEqual(uint64(0), tChild.Span.TraceID)
assert.NotEqual(uint64(0), tChild.Span.SpanID)
assert.Equal(tRoot.Span.SpanID, tChild.Span.ParentID)
assert.Equal(tRoot.Span.TraceID, tChild.Span.ParentID)
}
func TestTracerBaggagePropagation(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
root := tracer.StartSpan("web.request")
root.SetBaggageItem("key", "value")
child := tracer.StartSpan("db.query", opentracing.ChildOf(root.Context()))
context, ok := child.Context().(SpanContext)
assert.True(ok)
assert.Equal("value", context.baggage["key"])
}
func TestTracerBaggageImmutability(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
root := tracer.StartSpan("web.request")
root.SetBaggageItem("key", "value")
child := tracer.StartSpan("db.query", opentracing.ChildOf(root.Context()))
child.SetBaggageItem("key", "changed!")
parentContext, ok := root.Context().(SpanContext)
assert.True(ok)
childContext, ok := child.Context().(SpanContext)
assert.True(ok)
assert.Equal("value", parentContext.baggage["key"])
assert.Equal("changed!", childContext.baggage["key"])
}
func TestTracerSpanTags(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
tag := opentracing.Tag{Key: "key", Value: "value"}
span, ok := tracer.StartSpan("web.request", tag).(*Span)
assert.True(ok)
assert.Equal("value", span.Span.Meta["key"])
}
func TestTracerSpanGlobalTags(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
config.GlobalTags["key"] = "value"
tracer, _, _ := NewTracer(config)
span := tracer.StartSpan("web.request").(*Span)
assert.Equal("value", span.Span.Meta["key"])
child := tracer.StartSpan("db.query", opentracing.ChildOf(span.Context())).(*Span)
assert.Equal("value", child.Span.Meta["key"])
}
func TestTracerSpanStartTime(t *testing.T) {
assert := assert.New(t)
config := NewConfiguration()
tracer, _, _ := NewTracer(config)
startTime := time.Now().Add(-10 * time.Second)
span, ok := tracer.StartSpan("web.request", opentracing.StartTime(startTime)).(*Span)
assert.True(ok)
assert.Equal(startTime.UnixNano(), span.Span.Start)
}

View file

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

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

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

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

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

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

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

127
vendor/github.com/DataDog/dd-trace-go/tracer/buffer.go generated vendored Normal file
View file

@ -0,0 +1,127 @@
package tracer
import (
"sync"
)
const (
// spanBufferDefaultInitSize is the initial size of our trace buffer,
// by default we allocate for a handful of spans within the trace,
// reasonable as span is actually way bigger, and avoids re-allocating
// over and over. Could be fine-tuned at runtime.
spanBufferDefaultInitSize = 10
// spanBufferDefaultMaxSize is the maximum number of spans we keep in memory.
// This is to avoid memory leaks, if above that value, spans are randomly
// dropped and ignore, resulting in corrupted tracing data, but ensuring
// original program continues to work as expected.
spanBufferDefaultMaxSize = 1e5
)
type spanBuffer struct {
channels tracerChans
spans []*Span
finishedSpans int
initSize int
maxSize int
sync.RWMutex
}
func newSpanBuffer(channels tracerChans, initSize, maxSize int) *spanBuffer {
if initSize <= 0 {
initSize = spanBufferDefaultInitSize
}
if maxSize <= 0 {
maxSize = spanBufferDefaultMaxSize
}
return &spanBuffer{
channels: channels,
initSize: initSize,
maxSize: maxSize,
}
}
func (tb *spanBuffer) Push(span *Span) {
if tb == nil {
return
}
tb.Lock()
defer tb.Unlock()
if len(tb.spans) > 0 {
// if spanBuffer is full, forget span
if len(tb.spans) >= tb.maxSize {
tb.channels.pushErr(&errorSpanBufFull{Len: len(tb.spans)})
return
}
// if there's a trace ID mismatch, ignore span
if tb.spans[0].TraceID != span.TraceID {
tb.channels.pushErr(&errorTraceIDMismatch{Expected: tb.spans[0].TraceID, Actual: span.TraceID})
return
}
}
if tb.spans == nil {
tb.spans = make([]*Span, 0, tb.initSize)
}
tb.spans = append(tb.spans, span)
}
func (tb *spanBuffer) flushable() bool {
tb.RLock()
defer tb.RUnlock()
if len(tb.spans) == 0 {
return false
}
return tb.finishedSpans == len(tb.spans)
}
func (tb *spanBuffer) ack() {
tb.Lock()
defer tb.Unlock()
tb.finishedSpans++
}
func (tb *spanBuffer) doFlush() {
if !tb.flushable() {
return
}
tb.Lock()
defer tb.Unlock()
tb.channels.pushTrace(tb.spans)
tb.spans = nil
tb.finishedSpans = 0 // important, because a buffer can be used for several flushes
}
func (tb *spanBuffer) Flush() {
if tb == nil {
return
}
tb.doFlush()
}
func (tb *spanBuffer) AckFinish() {
if tb == nil {
return
}
tb.ack()
tb.doFlush()
}
func (tb *spanBuffer) Len() int {
if tb == nil {
return 0
}
tb.RLock()
defer tb.RUnlock()
return len(tb.spans)
}

View file

@ -0,0 +1,105 @@
package tracer
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const (
testInitSize = 2
testMaxSize = 5
)
func TestSpanBufferPushOne(t *testing.T) {
assert := assert.New(t)
buffer := newSpanBuffer(newTracerChans(), testInitSize, testMaxSize)
assert.NotNil(buffer)
assert.Len(buffer.spans, 0)
traceID := NextSpanID()
root := NewSpan("name1", "a-service", "a-resource", traceID, traceID, 0, nil)
root.buffer = buffer
buffer.Push(root)
assert.Len(buffer.spans, 1, "there is one span in the buffer")
assert.Equal(root, buffer.spans[0], "the span is the one pushed before")
root.Finish()
select {
case trace := <-buffer.channels.trace:
assert.Len(trace, 1, "there was a trace in the channel")
assert.Equal(root, trace[0], "the trace in the channel is the one pushed before")
assert.Equal(0, buffer.Len(), "no more spans in the buffer")
case err := <-buffer.channels.err:
assert.Fail("unexpected error:", err.Error())
t.Logf("buffer: %v", buffer)
}
}
func TestSpanBufferPushNoFinish(t *testing.T) {
assert := assert.New(t)
buffer := newSpanBuffer(newTracerChans(), testInitSize, testMaxSize)
assert.NotNil(buffer)
assert.Len(buffer.spans, 0)
traceID := NextSpanID()
root := NewSpan("name1", "a-service", "a-resource", traceID, traceID, 0, nil)
root.buffer = buffer
buffer.Push(root)
assert.Len(buffer.spans, 1, "there is one span in the buffer")
assert.Equal(root, buffer.spans[0], "the span is the one pushed before")
select {
case <-buffer.channels.trace:
assert.Fail("span was not finished, should not be flushed")
t.Logf("buffer: %v", buffer)
case err := <-buffer.channels.err:
assert.Fail("unexpected error:", err.Error())
t.Logf("buffer: %v", buffer)
case <-time.After(time.Second / 10):
t.Logf("expected timeout, nothing should show up in buffer as the trace is not finished")
}
}
func TestSpanBufferPushSeveral(t *testing.T) {
assert := assert.New(t)
buffer := newSpanBuffer(newTracerChans(), testInitSize, testMaxSize)
assert.NotNil(buffer)
assert.Len(buffer.spans, 0)
traceID := NextSpanID()
root := NewSpan("name1", "a-service", "a-resource", traceID, traceID, 0, nil)
span2 := NewSpan("name2", "a-service", "a-resource", NextSpanID(), traceID, root.SpanID, nil)
span3 := NewSpan("name3", "a-service", "a-resource", NextSpanID(), traceID, root.SpanID, nil)
span3a := NewSpan("name3", "a-service", "a-resource", NextSpanID(), traceID, span3.SpanID, nil)
spans := []*Span{root, span2, span3, span3a}
for i, span := range spans {
span.buffer = buffer
buffer.Push(span)
assert.Len(buffer.spans, i+1, "there is one more span in the buffer")
assert.Equal(span, buffer.spans[i], "the span is the one pushed before")
}
for _, span := range spans {
span.Finish()
}
select {
case trace := <-buffer.channels.trace:
assert.Len(trace, 4, "there was one trace with the right number of spans in the channel")
for _, span := range spans {
assert.Contains(trace, span, "the trace contains the spans")
}
case err := <-buffer.channels.err:
assert.Fail("unexpected error:", err.Error())
}
}

View file

@ -0,0 +1,88 @@
package tracer
const (
// traceChanLen is the capacity of the trace channel. This channels is emptied
// on a regular basis (worker thread) or when it reaches 50% of its capacity.
// If it's full, then data is simply dropped and ignored, with a log message.
// This only happens under heavy load,
traceChanLen = 1000
// serviceChanLen is the length of the service channel. As for the trace channel,
// it's emptied by worker thread or when it reaches 50%. Note that there should
// be much less data here, as service data does not be to be updated that often.
serviceChanLen = 50
// errChanLen is the number of errors we keep in the error channel. When this
// one is full, errors are just ignored, dropped, nothing left. At some point,
// there's already a whole lot of errors in the backlog, there's no real point
// in keeping millions of errors, a representative sample is enough. And we
// don't want to block user code and/or bloat memory or log files with redundant data.
errChanLen = 200
)
// traceChans holds most tracer channels together, it's mostly used to
// pass them together to the span buffer/context. It's obviously safe
// to access it concurrently as it contains channels only. And it's convenient
// to have it isolated from tracer, for the sake of unit testing.
type tracerChans struct {
trace chan []*Span
service chan Service
err chan error
traceFlush chan struct{}
serviceFlush chan struct{}
errFlush chan struct{}
}
func newTracerChans() tracerChans {
return tracerChans{
trace: make(chan []*Span, traceChanLen),
service: make(chan Service, serviceChanLen),
err: make(chan error, errChanLen),
traceFlush: make(chan struct{}, 1),
serviceFlush: make(chan struct{}, 1),
errFlush: make(chan struct{}, 1),
}
}
func (tc *tracerChans) pushTrace(trace []*Span) {
if len(tc.trace) >= cap(tc.trace)/2 { // starts being full, anticipate, try and flush soon
select {
case tc.traceFlush <- struct{}{}:
default: // a flush was already requested, skip
}
}
select {
case tc.trace <- trace:
default: // never block user code
tc.pushErr(&errorTraceChanFull{Len: len(tc.trace)})
}
}
func (tc *tracerChans) pushService(service Service) {
if len(tc.service) >= cap(tc.service)/2 { // starts being full, anticipate, try and flush soon
select {
case tc.serviceFlush <- struct{}{}:
default: // a flush was already requested, skip
}
}
select {
case tc.service <- service:
default: // never block user code
tc.pushErr(&errorServiceChanFull{Len: len(tc.service)})
}
}
func (tc *tracerChans) pushErr(err error) {
if len(tc.err) >= cap(tc.err)/2 { // starts being full, anticipate, try and flush soon
select {
case tc.errFlush <- struct{}{}:
default: // a flush was already requested, skip
}
}
select {
case tc.err <- err:
default:
// OK, if we get this, our error error buffer is full,
// we can assume it is filled with meaningful messages which
// are going to be logged and hopefully read, nothing better
// we can do, blocking would make things worse.
}
}

View file

@ -0,0 +1,117 @@
package tracer
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushTrace(t *testing.T) {
assert := assert.New(t)
channels := newTracerChans()
trace := []*Span{
&Span{
Name: "pylons.request",
Service: "pylons",
Resource: "/",
},
&Span{
Name: "pylons.request",
Service: "pylons",
Resource: "/foo",
},
}
channels.pushTrace(trace)
assert.Len(channels.trace, 1, "there should be data in channel")
assert.Len(channels.traceFlush, 0, "no flush requested yet")
pushed := <-channels.trace
assert.Equal(trace, pushed)
many := traceChanLen/2 + 1
for i := 0; i < many; i++ {
channels.pushTrace(make([]*Span, i))
}
assert.Len(channels.trace, many, "all traces should be in the channel, not yet blocking")
assert.Len(channels.traceFlush, 1, "a trace flush should have been requested")
for i := 0; i < cap(channels.trace); i++ {
channels.pushTrace(make([]*Span, i))
}
assert.Len(channels.trace, traceChanLen, "buffer should be full")
assert.NotEqual(0, len(channels.err), "there should be an error logged")
err := <-channels.err
assert.Equal(&errorTraceChanFull{Len: traceChanLen}, err)
}
func TestPushService(t *testing.T) {
assert := assert.New(t)
channels := newTracerChans()
service := Service{
Name: "redis-master",
App: "redis",
AppType: "db",
}
channels.pushService(service)
assert.Len(channels.service, 1, "there should be data in channel")
assert.Len(channels.serviceFlush, 0, "no flush requested yet")
pushed := <-channels.service
assert.Equal(service, pushed)
many := serviceChanLen/2 + 1
for i := 0; i < many; i++ {
channels.pushService(Service{
Name: fmt.Sprintf("service%d", i),
App: "custom",
AppType: "web",
})
}
assert.Len(channels.service, many, "all services should be in the channel, not yet blocking")
assert.Len(channels.serviceFlush, 1, "a service flush should have been requested")
for i := 0; i < cap(channels.service); i++ {
channels.pushService(Service{
Name: fmt.Sprintf("service%d", i),
App: "custom",
AppType: "web",
})
}
assert.Len(channels.service, serviceChanLen, "buffer should be full")
assert.NotEqual(0, len(channels.err), "there should be an error logged")
err := <-channels.err
assert.Equal(&errorServiceChanFull{Len: serviceChanLen}, err)
}
func TestPushErr(t *testing.T) {
assert := assert.New(t)
channels := newTracerChans()
err := fmt.Errorf("ooops")
channels.pushErr(err)
assert.Len(channels.err, 1, "there should be data in channel")
assert.Len(channels.errFlush, 0, "no flush requested yet")
pushed := <-channels.err
assert.Equal(err, pushed)
many := errChanLen/2 + 1
for i := 0; i < many; i++ {
channels.pushErr(fmt.Errorf("err %d", i))
}
assert.Len(channels.err, many, "all errs should be in the channel, not yet blocking")
assert.Len(channels.errFlush, 1, "a err flush should have been requested")
for i := 0; i < cap(channels.err); i++ {
channels.pushErr(fmt.Errorf("err %d", i))
}
// if we reach this, means pushErr is not blocking, which is what we want to double-check
}

View file

@ -0,0 +1,42 @@
package tracer
import (
"context"
)
var spanKey = "datadog_trace_span"
// ContextWithSpan will return a new context that includes the given span.
// DEPRECATED: use span.Context(ctx) instead.
func ContextWithSpan(ctx context.Context, span *Span) context.Context {
if span == nil {
return ctx
}
return span.Context(ctx)
}
// SpanFromContext returns the stored *Span from the Context if it's available.
// This helper returns also the ok value that is true if the span is present.
func SpanFromContext(ctx context.Context) (*Span, bool) {
if ctx == nil {
return nil, false
}
span, ok := ctx.Value(spanKey).(*Span)
return span, ok
}
// SpanFromContextDefault returns the stored *Span from the Context. If not, it
// will return an empty span that will do nothing.
func SpanFromContextDefault(ctx context.Context) *Span {
// FIXME[matt] is it better to return a singleton empty span?
if ctx == nil {
return &Span{}
}
span, ok := SpanFromContext(ctx)
if !ok {
return &Span{}
}
return span
}

View file

@ -0,0 +1,69 @@
package tracer
import (
"testing"
"context"
"github.com/stretchr/testify/assert"
)
func TestContextWithSpanDefault(t *testing.T) {
assert := assert.New(t)
// create a new context with a span
span := SpanFromContextDefault(nil)
assert.NotNil(span)
ctx := context.Background()
assert.NotNil(SpanFromContextDefault(ctx))
}
func TestSpanFromContext(t *testing.T) {
assert := assert.New(t)
// create a new context with a span
ctx := context.Background()
tracer := NewTracer()
expectedSpan := tracer.NewRootSpan("pylons.request", "pylons", "/")
ctx = ContextWithSpan(ctx, expectedSpan)
span, ok := SpanFromContext(ctx)
assert.True(ok)
assert.Equal(expectedSpan, span)
}
func TestSpanFromContextNil(t *testing.T) {
assert := assert.New(t)
// create a context without a span
ctx := context.Background()
span, ok := SpanFromContext(ctx)
assert.False(ok)
assert.Nil(span)
span, ok = SpanFromContext(nil)
assert.False(ok)
assert.Nil(span)
}
func TestSpanMissingParent(t *testing.T) {
assert := assert.New(t)
tracer := NewTracer()
// assuming we're in an inner function and we
// forget the nil or ok checks
ctx := context.Background()
span, _ := SpanFromContext(ctx)
// span is nil according to the API
child := tracer.NewChildSpan("redis.command", span)
child.Finish()
// the child is finished but it's not recorded in
// the tracer buffer because the service is missing
assert.True(child.Duration > 0)
assert.Equal(1, len(tracer.channels.trace))
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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