From 2b388d73af54ed823fc79a3aec9a456f2512d78a Mon Sep 17 00:00:00 2001 From: Sam Perrin Date: Mon, 24 Aug 2020 08:42:10 +0100 Subject: [PATCH 01/22] contrib/database/sql: add query_type span tag (#707) This change is to add the query type to the span tags (previously resource). This will help differentiate the spans whenever a `query` has been provided to `tryTrace` (e.g. Prepare and Query for a prepared statement). If a `query` has been provided then the `resource.name` gets set to the query and not the resource. Extends #270 --- contrib/database/sql/conn.go | 35 ++++++++++++++++++++--------- contrib/database/sql/stmt.go | 10 ++++----- contrib/database/sql/tx.go | 4 ++-- contrib/internal/sqltest/sqltest.go | 8 +++++++ 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/contrib/database/sql/conn.go b/contrib/database/sql/conn.go index 7312001684..5198fb7903 100644 --- a/contrib/database/sql/conn.go +++ b/contrib/database/sql/conn.go @@ -19,6 +19,19 @@ import ( var _ driver.Conn = (*tracedConn)(nil) +type queryType string + +const ( + queryTypeQuery queryType = "Query" + queryTypePing = "Ping" + queryTypePrepare = "Prepare" + queryTypeExec = "Exec" + queryTypeBegin = "Begin" + queryTypeClose = "Close" + queryTypeCommit = "Commit" + queryTypeRollback = "Rollback" +) + type tracedConn struct { driver.Conn *traceParams @@ -28,14 +41,14 @@ func (tc *tracedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx dr start := time.Now() if connBeginTx, ok := tc.Conn.(driver.ConnBeginTx); ok { tx, err = connBeginTx.BeginTx(ctx, opts) - tc.tryTrace(ctx, "Begin", "", start, err) + tc.tryTrace(ctx, queryTypeBegin, "", start, err) if err != nil { return nil, err } return &tracedTx{tx, tc.traceParams, ctx}, nil } tx, err = tc.Conn.Begin() - tc.tryTrace(ctx, "Begin", "", start, err) + tc.tryTrace(ctx, queryTypeBegin, "", start, err) if err != nil { return nil, err } @@ -46,14 +59,14 @@ func (tc *tracedConn) PrepareContext(ctx context.Context, query string) (stmt dr start := time.Now() if connPrepareCtx, ok := tc.Conn.(driver.ConnPrepareContext); ok { stmt, err := connPrepareCtx.PrepareContext(ctx, query) - tc.tryTrace(ctx, "Prepare", query, start, err) + tc.tryTrace(ctx, queryTypePrepare, query, start, err) if err != nil { return nil, err } return &tracedStmt{stmt, tc.traceParams, ctx, query}, nil } stmt, err = tc.Prepare(query) - tc.tryTrace(ctx, "Prepare", query, start, err) + tc.tryTrace(ctx, queryTypePrepare, query, start, err) if err != nil { return nil, err } @@ -71,7 +84,7 @@ func (tc *tracedConn) ExecContext(ctx context.Context, query string, args []driv start := time.Now() if execContext, ok := tc.Conn.(driver.ExecerContext); ok { r, err := execContext.ExecContext(ctx, query, args) - tc.tryTrace(ctx, "Exec", query, start, err) + tc.tryTrace(ctx, queryTypeExec, query, start, err) return r, err } dargs, err := namedValueToValue(args) @@ -84,7 +97,7 @@ func (tc *tracedConn) ExecContext(ctx context.Context, query string, args []driv default: } r, err = tc.Exec(query, dargs) - tc.tryTrace(ctx, "Exec", query, start, err) + tc.tryTrace(ctx, queryTypeExec, query, start, err) return r, err } @@ -94,7 +107,7 @@ func (tc *tracedConn) Ping(ctx context.Context) (err error) { if pinger, ok := tc.Conn.(driver.Pinger); ok { err = pinger.Ping(ctx) } - tc.tryTrace(ctx, "Ping", "", start, err) + tc.tryTrace(ctx, queryTypePing, "", start, err) return err } @@ -109,7 +122,7 @@ func (tc *tracedConn) QueryContext(ctx context.Context, query string, args []dri start := time.Now() if queryerContext, ok := tc.Conn.(driver.QueryerContext); ok { rows, err := queryerContext.QueryContext(ctx, query, args) - tc.tryTrace(ctx, "Query", query, start, err) + tc.tryTrace(ctx, queryTypeQuery, query, start, err) return rows, err } dargs, err := namedValueToValue(args) @@ -122,7 +135,7 @@ func (tc *tracedConn) QueryContext(ctx context.Context, query string, args []dri default: } rows, err = tc.Query(query, dargs) - tc.tryTrace(ctx, "Query", query, start, err) + tc.tryTrace(ctx, queryTypeQuery, query, start, err) return rows, err } @@ -151,7 +164,7 @@ func WithSpanTags(ctx context.Context, tags map[string]string) context.Context { } // tryTrace will create a span using the given arguments, but will act as a no-op when err is driver.ErrSkip. -func (tp *traceParams) tryTrace(ctx context.Context, resource string, query string, startTime time.Time, err error) { +func (tp *traceParams) tryTrace(ctx context.Context, qtype queryType, query string, startTime time.Time, err error) { if err == driver.ErrSkip { // Not a user error: driver is telling sql package that an // optional interface method is not implemented. There is @@ -169,9 +182,11 @@ func (tp *traceParams) tryTrace(ctx context.Context, resource string, query stri opts = append(opts, tracer.Tag(ext.EventSampleRate, tp.cfg.analyticsRate)) } span, _ := tracer.StartSpanFromContext(ctx, name, opts...) + resource := string(qtype) if query != "" { resource = query } + span.SetTag("sql.query_type", string(qtype)) span.SetTag(ext.ResourceName, resource) for k, v := range tp.meta { span.SetTag(k, v) diff --git a/contrib/database/sql/stmt.go b/contrib/database/sql/stmt.go index b68943e950..5d097087eb 100644 --- a/contrib/database/sql/stmt.go +++ b/contrib/database/sql/stmt.go @@ -26,7 +26,7 @@ type tracedStmt struct { func (s *tracedStmt) Close() (err error) { start := time.Now() err = s.Stmt.Close() - s.tryTrace(s.ctx, "Close", "", start, err) + s.tryTrace(s.ctx, queryTypeClose, "", start, err) return err } @@ -35,7 +35,7 @@ func (s *tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue) start := time.Now() if stmtExecContext, ok := s.Stmt.(driver.StmtExecContext); ok { res, err := stmtExecContext.ExecContext(ctx, args) - s.tryTrace(ctx, "Exec", s.query, start, err) + s.tryTrace(ctx, queryTypeExec, s.query, start, err) return res, err } dargs, err := namedValueToValue(args) @@ -48,7 +48,7 @@ func (s *tracedStmt) ExecContext(ctx context.Context, args []driver.NamedValue) default: } res, err = s.Exec(dargs) - s.tryTrace(ctx, "Exec", s.query, start, err) + s.tryTrace(ctx, queryTypeExec, s.query, start, err) return res, err } @@ -57,7 +57,7 @@ func (s *tracedStmt) QueryContext(ctx context.Context, args []driver.NamedValue) start := time.Now() if stmtQueryContext, ok := s.Stmt.(driver.StmtQueryContext); ok { rows, err := stmtQueryContext.QueryContext(ctx, args) - s.tryTrace(ctx, "Query", s.query, start, err) + s.tryTrace(ctx, queryTypeQuery, s.query, start, err) return rows, err } dargs, err := namedValueToValue(args) @@ -70,7 +70,7 @@ func (s *tracedStmt) QueryContext(ctx context.Context, args []driver.NamedValue) default: } rows, err = s.Query(dargs) - s.tryTrace(ctx, "Query", s.query, start, err) + s.tryTrace(ctx, queryTypeQuery, s.query, start, err) return rows, err } diff --git a/contrib/database/sql/tx.go b/contrib/database/sql/tx.go index c3fb028e5e..c66be61fae 100644 --- a/contrib/database/sql/tx.go +++ b/contrib/database/sql/tx.go @@ -24,7 +24,7 @@ type tracedTx struct { func (t *tracedTx) Commit() (err error) { start := time.Now() err = t.Tx.Commit() - t.tryTrace(t.ctx, "Commit", "", start, err) + t.tryTrace(t.ctx, queryTypeCommit, "", start, err) return err } @@ -32,6 +32,6 @@ func (t *tracedTx) Commit() (err error) { func (t *tracedTx) Rollback() (err error) { start := time.Now() err = t.Tx.Rollback() - t.tryTrace(t.ctx, "Rollback", "", start, err) + t.tryTrace(t.ctx, queryTypeRollback, "", start, err) return err } diff --git a/contrib/internal/sqltest/sqltest.go b/contrib/internal/sqltest/sqltest.go index 555910a412..162ef149ca 100644 --- a/contrib/internal/sqltest/sqltest.go +++ b/contrib/internal/sqltest/sqltest.go @@ -71,6 +71,7 @@ func testPing(cfg *Config) func(*testing.T) { span := spans[0] assert.Equal(cfg.ExpectName, span.OperationName()) + cfg.ExpectTags["sql.query_type"] = "Ping" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -90,6 +91,7 @@ func testQuery(cfg *Config) func(*testing.T) { assert.Len(spans, 1) span := spans[0] + cfg.ExpectTags["sql.query_type"] = "Query" assert.Equal(cfg.ExpectName, span.OperationName()) for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) @@ -116,6 +118,7 @@ func testStatement(cfg *Config) func(*testing.T) { span := spans[0] assert.Equal(cfg.ExpectName, span.OperationName()) + cfg.ExpectTags["sql.query_type"] = "Prepare" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -128,6 +131,7 @@ func testStatement(cfg *Config) func(*testing.T) { assert.Len(spans, 1) span = spans[0] assert.Equal(cfg.ExpectName, span.OperationName()) + cfg.ExpectTags["sql.query_type"] = "Exec" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -147,6 +151,7 @@ func testBeginRollback(cfg *Config) func(*testing.T) { span := spans[0] assert.Equal(cfg.ExpectName, span.OperationName()) + cfg.ExpectTags["sql.query_type"] = "Begin" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -159,6 +164,7 @@ func testBeginRollback(cfg *Config) func(*testing.T) { assert.Len(spans, 1) span = spans[0] assert.Equal(cfg.ExpectName, span.OperationName()) + cfg.ExpectTags["sql.query_type"] = "Rollback" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -195,6 +201,7 @@ func testExec(cfg *Config) func(*testing.T) { } } assert.NotNil(span, "span not found") + cfg.ExpectTags["sql.query_type"] = "Exec" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } @@ -204,6 +211,7 @@ func testExec(cfg *Config) func(*testing.T) { } } assert.NotNil(span, "span not found") + cfg.ExpectTags["sql.query_type"] = "Commit" for k, v := range cfg.ExpectTags { assert.Equal(v, span.Tag(k), "Value mismatch on tag %s", k) } From 89abe80709322668d7461887f25cbdd250fbba4e Mon Sep 17 00:00:00 2001 From: Adrien Date: Mon, 24 Aug 2020 10:35:12 +0200 Subject: [PATCH 02/22] contrib/gomodule/redigo: ensure context is never nil (#711) Ensure context is never nil. This causes panic in go1.15 Closes #710 --- contrib/gomodule/redigo/redigo.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/gomodule/redigo/redigo.go b/contrib/gomodule/redigo/redigo.go index cec2b06fc8..6d348377bb 100644 --- a/contrib/gomodule/redigo/redigo.go +++ b/contrib/gomodule/redigo/redigo.go @@ -136,6 +136,11 @@ func withSpan(do func(commandName string, args ...interface{}) (interface{}, err } } + if ctx == nil { + // Passing a nil context was working up to Go 1.15, where issue #37908 + // was closed. + ctx = context.Background() + } span := newChildSpan(ctx, p) defer func() { span.Finish(tracer.WithError(err)) From 83bce6d7701f2aa95d90025ce161dcdafb3f46e3 Mon Sep 17 00:00:00 2001 From: Mike F <52503840+patronmike@users.noreply.github.com> Date: Tue, 1 Sep 2020 01:55:55 -0500 Subject: [PATCH 03/22] contrib/gopkg.in/jinzhu/gorm.v1: add support (#713) Closes #712 --- .../gopkg.in/jinzhu/gorm.v1/example_test.go | 36 +++ contrib/gopkg.in/jinzhu/gorm.v1/gorm.go | 130 ++++++++ contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go | 305 ++++++++++++++++++ contrib/gopkg.in/jinzhu/gorm.v1/option.go | 62 ++++ 4 files changed, 533 insertions(+) create mode 100644 contrib/gopkg.in/jinzhu/gorm.v1/example_test.go create mode 100644 contrib/gopkg.in/jinzhu/gorm.v1/gorm.go create mode 100644 contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go create mode 100644 contrib/gopkg.in/jinzhu/gorm.v1/option.go diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/example_test.go b/contrib/gopkg.in/jinzhu/gorm.v1/example_test.go new file mode 100644 index 0000000000..7223aa60b4 --- /dev/null +++ b/contrib/gopkg.in/jinzhu/gorm.v1/example_test.go @@ -0,0 +1,36 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package gorm_test + +import ( + "log" + + "github.com/lib/pq" + "gopkg.in/jinzhu/gorm.v1" + + sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" + gormtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gopkg.in/jinzhu/gorm.v1" +) + +func ExampleOpen() { + // Register augments the provided driver with tracing, enabling it to be loaded by gormtrace.Open. + sqltrace.Register("postgres", &pq.Driver{}, sqltrace.WithServiceName("my-service")) + + // Open the registered driver, allowing all uses of the returned *gorm.DB to be traced. + db, err := gormtrace.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable") + defer db.Close() + if err != nil { + log.Fatal(err) + } + + user := struct { + gorm.Model + Name string + }{} + + // All calls through gorm.DB are now traced. + db.Where("name = ?", "jinzhu").First(&user) +} diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go b/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go new file mode 100644 index 0000000000..abcc8469ab --- /dev/null +++ b/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go @@ -0,0 +1,130 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +// Package gorm provides helper functions for tracing the jinzhu/gorm package (https://github.com/jinzhu/gorm). +package gorm + +import ( + "context" + "math" + "time" + + sqltraced "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "gopkg.in/jinzhu/gorm.v1" +) + +const ( + gormContextKey = "dd-trace-go:context" + gormConfigKey = "dd-trace-go:config" + gormSpanStartTimeKey = "dd-trace-go:span" +) + +// Open opens a new (traced) database connection. The used dialect must be formerly registered +// using (gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql).Register. +func Open(dialect, source string, opts ...Option) (*gorm.DB, error) { + sqldb, err := sqltraced.Open(dialect, source) + if err != nil { + return nil, err + } + db, err := gorm.Open(dialect, sqldb) + if err != nil { + return db, err + } + return WithCallbacks(db, opts...), err +} + +// WithCallbacks registers callbacks to the gorm.DB for tracing. +// It should be called once, after opening the db. +// The callbacks are triggered by Create, Update, Delete, +// Query and RowQuery operations. +func WithCallbacks(db *gorm.DB, opts ...Option) *gorm.DB { + afterFunc := func(operationName string) func(*gorm.Scope) { + return func(scope *gorm.Scope) { + after(scope, operationName) + } + } + + cb := db.Callback() + cb.Create().Before("gorm:before_create").Register("dd-trace-go:before_create", before) + cb.Create().After("gorm:after_create").Register("dd-trace-go:after_create", afterFunc("gorm.create")) + cb.Update().Before("gorm:before_update").Register("dd-trace-go:before_update", before) + cb.Update().After("gorm:after_update").Register("dd-trace-go:after_update", afterFunc("gorm.update")) + cb.Delete().Before("gorm:before_delete").Register("dd-trace-go:before_delete", before) + cb.Delete().After("gorm:after_delete").Register("dd-trace-go:after_delete", afterFunc("gorm.delete")) + cb.Query().Before("gorm:query").Register("dd-trace-go:before_query", before) + cb.Query().After("gorm:after_query").Register("dd-trace-go:after_query", afterFunc("gorm.query")) + cb.RowQuery().Before("gorm:row_query").Register("dd-trace-go:before_row_query", before) + cb.RowQuery().After("gorm:row_query").Register("dd-trace-go:after_row_query", afterFunc("gorm.row_query")) + + cfg := new(config) + defaults(cfg) + for _, fn := range opts { + fn(cfg) + } + return db.Set(gormConfigKey, cfg) +} + +// WithContext attaches the specified context to the given db. The context will +// be used as a basis for creating new spans. An example use case is providing +// a context which contains a span to be used as a parent. +func WithContext(ctx context.Context, db *gorm.DB) *gorm.DB { + if ctx == nil { + return db + } + db = db.Set(gormContextKey, ctx) + return db +} + +// ContextFromDB returns any context previously attached to db using WithContext, +// otherwise returning context.Background. +func ContextFromDB(db *gorm.DB) context.Context { + if v, ok := db.Get(gormContextKey); ok { + if ctx, ok := v.(context.Context); ok { + return ctx + } + } + return context.Background() +} + +func before(scope *gorm.Scope) { + scope.Set(gormSpanStartTimeKey, time.Now()) +} + +func after(scope *gorm.Scope, operationName string) { + v, ok := scope.Get(gormContextKey) + if !ok { + return + } + ctx := v.(context.Context) + + v, ok = scope.Get(gormConfigKey) + if !ok { + return + } + cfg := v.(*config) + + v, ok = scope.Get(gormSpanStartTimeKey) + if !ok { + return + } + t, ok := v.(time.Time) + + opts := []ddtrace.StartSpanOption{ + tracer.StartTime(t), + tracer.ServiceName(cfg.serviceName), + tracer.SpanType(ext.SpanTypeSQL), + tracer.ResourceName(scope.SQL), + } + if !math.IsNaN(cfg.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) + } + + span, _ := tracer.StartSpanFromContext(ctx, operationName, opts...) + span.Finish(tracer.WithError(scope.DB().Error)) +} diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go b/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go new file mode 100644 index 0000000000..87140b35bf --- /dev/null +++ b/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go @@ -0,0 +1,305 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package gorm + +import ( + "context" + "fmt" + "log" + "os" + "testing" + + sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/sqltest" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + + "github.com/go-sql-driver/mysql" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "gopkg.in/jinzhu/gorm.v1" +) + +// tableName holds the SQL table that these tests will be run against. It must be unique cross-repo. +const tableName = "testgorm" + +func TestMain(m *testing.M) { + _, ok := os.LookupEnv("INTEGRATION") + if !ok { + fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable") + os.Exit(0) + } + defer sqltest.Prepare(tableName)() + os.Exit(m.Run()) +} + +func TestMySQL(t *testing.T) { + sqltrace.Register("mysql", &mysql.MySQLDriver{}, sqltrace.WithServiceName("mysql-test")) + db, err := Open("mysql", "test:test@tcp(127.0.0.1:3306)/test") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + testConfig := &sqltest.Config{ + DB: db.DB(), + DriverName: "mysql", + TableName: tableName, + ExpectName: "mysql.query", + ExpectTags: map[string]interface{}{ + ext.ServiceName: "mysql-test", + ext.SpanType: ext.SpanTypeSQL, + ext.TargetHost: "127.0.0.1", + ext.TargetPort: "3306", + "db.user": "test", + "db.name": "test", + }, + } + sqltest.RunAll(t, testConfig) +} + +func TestPostgres(t *testing.T) { + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + testConfig := &sqltest.Config{ + DB: db.DB(), + DriverName: "postgres", + TableName: tableName, + ExpectName: "postgres.query", + ExpectTags: map[string]interface{}{ + ext.ServiceName: "postgres.db", + ext.SpanType: ext.SpanTypeSQL, + ext.TargetHost: "127.0.0.1", + ext.TargetPort: "5432", + "db.user": "postgres", + "db.name": "postgres", + }, + } + sqltest.RunAll(t, testConfig) +} + +type Product struct { + gorm.Model + Code string + Price uint +} + +func TestCallbacks(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + if err != nil { + log.Fatal(err) + } + defer db.Close() + db.AutoMigrate(&Product{}) + + t.Run("create", func(t *testing.T) { + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + db.Create(&Product{Code: "L1212", Price: 1000}) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 3) + + span := spans[len(spans)-3] + assert.Equal("gorm.create", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal( + `INSERT INTO "products" ("created_at","updated_at","deleted_at","code","price") VALUES ($1,$2,$3,$4,$5) RETURNING "products"."id"`, + span.Tag(ext.ResourceName)) + }) + + t.Run("query", func(t *testing.T) { + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + var product Product + db.First(&product, "code = ?", "L1212") + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 2) + + span := spans[len(spans)-2] + assert.Equal("gorm.query", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal( + `SELECT * FROM "products" WHERE "products"."deleted_at" IS NULL AND ((code = $1)) ORDER BY "products"."id" ASC LIMIT 1`, + span.Tag(ext.ResourceName)) + }) + + t.Run("update", func(t *testing.T) { + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + var product Product + db.First(&product, "code = ?", "L1212") + db.Model(&product).Update("Price", 2000) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 3) + + span := spans[len(spans)-3] + assert.Equal("gorm.update", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal( + `UPDATE "products" SET "price" = $1, "updated_at" = $2 WHERE "products"."deleted_at" IS NULL AND "products"."id" = $3`, + span.Tag(ext.ResourceName)) + }) + + t.Run("delete", func(t *testing.T) { + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + var product Product + db.First(&product, "code = ?", "L1212") + db.Delete(&product) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 3) + + span := spans[len(spans)-3] + assert.Equal("gorm.delete", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal( + `UPDATE "products" SET "deleted_at"=$1 WHERE "products"."deleted_at" IS NULL AND "products"."id" = $2`, + span.Tag(ext.ResourceName)) + }) +} + +func TestAnalyticsSettings(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + if err != nil { + log.Fatal(err) + } + defer db.Close() + db.AutoMigrate(&Product{}) + + assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...Option) { + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable", opts...) + if err != nil { + log.Fatal(err) + } + defer db.Close() + db.AutoMigrate(&Product{}) + + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + db.Create(&Product{Code: "L1212", Price: 1000}) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(t, len(spans) > 3) + s := spans[len(spans)-3] + assert.Equal(t, rate, s.Tag(ext.EventSampleRate)) + } + + t.Run("defaults", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil) + }) + + t.Run("global", func(t *testing.T) { + t.Skip("global flag disabled") + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.4) + }) + + t.Run("enabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, 1.0, WithAnalytics(true)) + }) + + t.Run("disabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil, WithAnalytics(false)) + }) + + t.Run("override", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.23, WithAnalyticsRate(0.23)) + }) +} + +func TestContext(t *testing.T) { + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + t.Run("with", func(t *testing.T) { + type key string + testCtx := context.WithValue(context.Background(), key("test context"), true) + db := WithContext(testCtx, db) + ctx := ContextFromDB(db) + assert.Equal(t, testCtx, ctx) + }) + + t.Run("without", func(t *testing.T) { + ctx := ContextFromDB(db) + assert.Equal(t, context.Background(), ctx) + }) +} diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/option.go b/contrib/gopkg.in/jinzhu/gorm.v1/option.go new file mode 100644 index 0000000000..7b799adbfe --- /dev/null +++ b/contrib/gopkg.in/jinzhu/gorm.v1/option.go @@ -0,0 +1,62 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package gorm + +import ( + "math" + + "gopkg.in/DataDog/dd-trace-go.v1/internal" +) + +type config struct { + serviceName string + analyticsRate float64 + dsn string +} + +// Option represents an option that can be passed to Register, Open or OpenDB. +type Option func(*config) + +func defaults(cfg *config) { + cfg.serviceName = "gorm.db" + // cfg.analyticsRate = globalconfig.AnalyticsRate() + if internal.BoolEnv("DD_TRACE_GORM_ANALYTICS_ENABLED", false) { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } +} + +// WithServiceName sets the given service name when registering a driver, +// or opening a database connection. +func WithServiceName(name string) Option { + return func(cfg *config) { + cfg.serviceName = name + } +} + +// WithAnalytics enables Trace Analytics for all started spans. +func WithAnalytics(on bool) Option { + return func(cfg *config) { + if on { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } + } +} + +// WithAnalyticsRate sets the sampling rate for Trace Analytics events +// correlated to started spans. +func WithAnalyticsRate(rate float64) Option { + return func(cfg *config) { + if rate >= 0.0 && rate <= 1.0 { + cfg.analyticsRate = rate + } else { + cfg.analyticsRate = math.NaN() + } + } +} From dff70cd623d743bdfe0ceb7c9e98c4d0a041ba3e Mon Sep 17 00:00:00 2001 From: Jacob Martin Date: Tue, 1 Sep 2020 10:24:29 +0200 Subject: [PATCH 04/22] contrib/graph-gophers/graphql-go: Add WithOmitTrivial option (#715) --- contrib/graph-gophers/graphql-go/graphql.go | 3 + .../graph-gophers/graphql-go/graphql_test.go | 122 +++++++++++++----- contrib/graph-gophers/graphql-go/option.go | 8 ++ 3 files changed, 102 insertions(+), 31 deletions(-) diff --git a/contrib/graph-gophers/graphql-go/graphql.go b/contrib/graph-gophers/graphql-go/graphql.go index ef7d05caaa..fb30d9fd88 100644 --- a/contrib/graph-gophers/graphql-go/graphql.go +++ b/contrib/graph-gophers/graphql-go/graphql.go @@ -68,6 +68,9 @@ func (t *Tracer) TraceQuery(ctx context.Context, queryString string, operationNa // TraceField traces a GraphQL field access. func (t *Tracer) TraceField(ctx context.Context, label string, typeName string, fieldName string, trivial bool, args map[string]interface{}) (context.Context, trace.TraceFieldFinishFunc) { + if t.cfg.omitTrivial && trivial { + return ctx, func(queryError *errors.QueryError) {} + } opts := []ddtrace.StartSpanOption{ tracer.ServiceName(t.cfg.serviceName), tracer.Tag(tagGraphqlField, fieldName), diff --git a/contrib/graph-gophers/graphql-go/graphql_test.go b/contrib/graph-gophers/graphql-go/graphql_test.go index f9d8df5809..c8e5dad71f 100644 --- a/contrib/graph-gophers/graphql-go/graphql_test.go +++ b/contrib/graph-gophers/graphql-go/graphql_test.go @@ -21,7 +21,8 @@ import ( type testResolver struct{} -func (*testResolver) Hello() string { return "Hello, world!" } +func (*testResolver) Hello() string { return "Hello, world!" } +func (*testResolver) HelloNonTrivial() (string, error) { return "Hello, world!", nil } func Test(t *testing.T) { s := ` @@ -30,44 +31,103 @@ func Test(t *testing.T) { } type Query { hello: String! + helloNonTrivial: String! } ` - schema := graphql.MustParseSchema(s, new(testResolver), - graphql.Tracer(NewTracer(WithServiceName("test-graphql-service")))) - srv := httptest.NewServer(&relay.Handler{Schema: schema}) - defer srv.Close() + makeRequest := func(opts ...Option) { + opts = append(opts, WithServiceName("test-graphql-service")) - mt := mocktracer.Start() - defer mt.Stop() + schema := graphql.MustParseSchema(s, new(testResolver), + graphql.Tracer(NewTracer(opts...))) + srv := httptest.NewServer(&relay.Handler{Schema: schema}) + defer srv.Close() - http.Post(srv.URL, "application/json", strings.NewReader(`{ - "query": "query TestQuery() { hello }", + http.Post(srv.URL, "application/json", strings.NewReader(`{ + "query": "query TestQuery() { hello, helloNonTrivial }", "operationName": "TestQuery" }`)) - - spans := mt.FinishedSpans() - assert.Len(t, spans, 2) - assert.Equal(t, spans[1].TraceID(), spans[0].TraceID()) - - { - s := spans[0] - assert.Equal(t, "hello", s.Tag(tagGraphqlField)) - assert.Nil(t, s.Tag(ext.Error)) - assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) - assert.Equal(t, "Query", s.Tag(tagGraphqlType)) - assert.Equal(t, "graphql.field", s.OperationName()) - assert.Equal(t, "graphql.field", s.Tag(ext.ResourceName)) } - { - s := spans[1] - assert.Equal(t, "query TestQuery() { hello }", s.Tag(tagGraphqlQuery)) - assert.Equal(t, "TestQuery", s.Tag(tagGraphqlOperationName)) - assert.Nil(t, s.Tag(ext.Error)) - assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) - assert.Equal(t, "graphql.request", s.OperationName()) - assert.Equal(t, "graphql.request", s.Tag(ext.ResourceName)) - } + t.Run("defaults", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + makeRequest() + + spans := mt.FinishedSpans() + assert.Len(t, spans, 3) + assert.Equal(t, spans[1].TraceID(), spans[0].TraceID()) + assert.Equal(t, spans[2].TraceID(), spans[0].TraceID()) + + // The order of the spans isn't deterministic. + helloSpanIndex := 0 + helloNonTrivialSpanIndex := 1 + if spans[0].Tag(tagGraphqlField) == "helloNonTrivial" { + helloNonTrivialSpanIndex = 0 + helloSpanIndex = 1 + } + + { + s := spans[helloNonTrivialSpanIndex] + assert.Equal(t, "helloNonTrivial", s.Tag(tagGraphqlField)) + assert.Nil(t, s.Tag(ext.Error)) + assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) + assert.Equal(t, "Query", s.Tag(tagGraphqlType)) + assert.Equal(t, "graphql.field", s.OperationName()) + assert.Equal(t, "graphql.field", s.Tag(ext.ResourceName)) + } + + { + s := spans[helloSpanIndex] + assert.Equal(t, "hello", s.Tag(tagGraphqlField)) + assert.Nil(t, s.Tag(ext.Error)) + assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) + assert.Equal(t, "Query", s.Tag(tagGraphqlType)) + assert.Equal(t, "graphql.field", s.OperationName()) + assert.Equal(t, "graphql.field", s.Tag(ext.ResourceName)) + } + + { + s := spans[2] + assert.Equal(t, "query TestQuery() { hello, helloNonTrivial }", s.Tag(tagGraphqlQuery)) + assert.Equal(t, "TestQuery", s.Tag(tagGraphqlOperationName)) + assert.Nil(t, s.Tag(ext.Error)) + assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) + assert.Equal(t, "graphql.request", s.OperationName()) + assert.Equal(t, "graphql.request", s.Tag(ext.ResourceName)) + } + }) + + t.Run("WithOmitTrivial", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + makeRequest(WithOmitTrivial()) + + spans := mt.FinishedSpans() + assert.Len(t, spans, 2) + assert.Equal(t, spans[1].TraceID(), spans[0].TraceID()) + + { + s := spans[0] + assert.Equal(t, "helloNonTrivial", s.Tag(tagGraphqlField)) + assert.Nil(t, s.Tag(ext.Error)) + assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) + assert.Equal(t, "Query", s.Tag(tagGraphqlType)) + assert.Equal(t, "graphql.field", s.OperationName()) + assert.Equal(t, "graphql.field", s.Tag(ext.ResourceName)) + } + + { + s := spans[1] + assert.Equal(t, "query TestQuery() { hello, helloNonTrivial }", s.Tag(tagGraphqlQuery)) + assert.Equal(t, "TestQuery", s.Tag(tagGraphqlOperationName)) + assert.Nil(t, s.Tag(ext.Error)) + assert.Equal(t, "test-graphql-service", s.Tag(ext.ServiceName)) + assert.Equal(t, "graphql.request", s.OperationName()) + assert.Equal(t, "graphql.request", s.Tag(ext.ResourceName)) + } + }) } func TestAnalyticsSettings(t *testing.T) { diff --git a/contrib/graph-gophers/graphql-go/option.go b/contrib/graph-gophers/graphql-go/option.go index d6267e57a9..d9909c622a 100644 --- a/contrib/graph-gophers/graphql-go/option.go +++ b/contrib/graph-gophers/graphql-go/option.go @@ -15,6 +15,7 @@ import ( type config struct { serviceName string analyticsRate float64 + omitTrivial bool } // Option represents an option that can be used customize the Tracer. @@ -62,3 +63,10 @@ func WithAnalyticsRate(rate float64) Option { } } } + +// WithOmitTrivial enables omission of graphql fields marked as trivial. +func WithOmitTrivial() Option { + return func(cfg *config) { + cfg.omitTrivial = true + } +} From 91357619bff0a530460befc2f487498e2084f3f3 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Mon, 7 Sep 2020 02:57:57 -0500 Subject: [PATCH 05/22] ddtrace/tracer: improve agent connectivity check (#719) This change ensures that the agent connectivity check is a valid request. Previously, an invalid request would cause confusing messages in the trace-agent logs (missing headers, invalid or empty body, etc). Fixes #705 --- ddtrace/tracer/log.go | 5 ++++- ddtrace/tracer/transport.go | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ddtrace/tracer/log.go b/ddtrace/tracer/log.go index 7e494319f0..709c4a3ed1 100644 --- a/ddtrace/tracer/log.go +++ b/ddtrace/tracer/log.go @@ -6,6 +6,7 @@ package tracer import ( + "bytes" "encoding/json" "fmt" "math" @@ -51,10 +52,12 @@ type startupInfo struct { // If the endpoint is not reachable, checkEndpoint returns an error // explaining why. func checkEndpoint(endpoint string) error { - req, err := http.NewRequest("POST", endpoint, nil) + req, err := http.NewRequest("POST", endpoint, bytes.NewReader([]byte{0x90})) if err != nil { return fmt.Errorf("cannot create http request: %v", err) } + req.Header.Set(traceCountHeader, "0") + req.Header.Set("Content-Type", "application/msgpack") _, err = defaultClient.Do(req) if err != nil { return err diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index 7fa506e468..ce459e6edf 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -58,9 +58,9 @@ type transport interface { // newTransport returns a new Transport implementation that sends traces to a // trace agent running on the given hostname and port, using a given -// http.RoundTripper. If the zero values for hostname and port are provided, +// *http.Client. If the zero values for hostname and port are provided, // the default values will be used ("localhost" for hostname, and "8126" for -// port). If roundTripper is nil, a default is used. +// port). If client is nil, a default is used. // // In general, using this method is only necessary if you have a trace agent // running on a non-default port, if it's located on another machine, or when From 498a4553f2152eccca152064c4929332a66b98f3 Mon Sep 17 00:00:00 2001 From: Jacob Martin Date: Tue, 8 Sep 2020 13:11:56 +0200 Subject: [PATCH 06/22] contrib/jinzhu/gorm: allow custom tags. (#723) This change allows setting custom tags via a callback with access to *gorm.Scope. The new option is called WithCustomTag. Closes #716 --- contrib/gopkg.in/jinzhu/gorm.v1/gorm.go | 5 +++ contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go | 41 ++++++++++++++++++++ contrib/gopkg.in/jinzhu/gorm.v1/option.go | 14 +++++++ contrib/jinzhu/gorm/gorm.go | 5 +++ contrib/jinzhu/gorm/gorm_test.go | 41 ++++++++++++++++++++ contrib/jinzhu/gorm/option.go | 14 +++++++ 6 files changed, 120 insertions(+) diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go b/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go index abcc8469ab..6aebb23334 100644 --- a/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go +++ b/contrib/gopkg.in/jinzhu/gorm.v1/gorm.go @@ -124,6 +124,11 @@ func after(scope *gorm.Scope, operationName string) { if !math.IsNaN(cfg.analyticsRate) { opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } + if cfg.tagFns != nil { + for key, tagFn := range cfg.tagFns { + opts = append(opts, tracer.Tag(key, tagFn(scope))) + } + } span, _ := tracer.StartSpanFromContext(ctx, operationName, opts...) span.Finish(tracer.WithError(scope.DB().Error)) diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go b/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go index 87140b35bf..89808386a9 100644 --- a/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go +++ b/contrib/gopkg.in/jinzhu/gorm.v1/gorm_test.go @@ -303,3 +303,44 @@ func TestContext(t *testing.T) { assert.Equal(t, context.Background(), ctx) }) } + +func TestCustomTags(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable", + WithCustomTag("custom_tag", func(scope *gorm.Scope) interface{} { + return scope.SQLVars[3] + }), + ) + if err != nil { + log.Fatal(err) + } + defer db.Close() + db.AutoMigrate(&Product{}) + + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + db.Create(&Product{Code: "L1212", Price: 1000}) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 3) + + // We deterministically expect the span to be the third last, + // followed by the underlying postgres DB trace and the above http.request span. + span := spans[len(spans)-3] + assert.Equal("gorm.create", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal("L1212", span.Tag("custom_tag")) + assert.Equal( + `INSERT INTO "products" ("created_at","updated_at","deleted_at","code","price") VALUES ($1,$2,$3,$4,$5) RETURNING "products"."id"`, + span.Tag(ext.ResourceName)) +} diff --git a/contrib/gopkg.in/jinzhu/gorm.v1/option.go b/contrib/gopkg.in/jinzhu/gorm.v1/option.go index 7b799adbfe..0515cf2562 100644 --- a/contrib/gopkg.in/jinzhu/gorm.v1/option.go +++ b/contrib/gopkg.in/jinzhu/gorm.v1/option.go @@ -9,12 +9,15 @@ import ( "math" "gopkg.in/DataDog/dd-trace-go.v1/internal" + + "gopkg.in/jinzhu/gorm.v1" ) type config struct { serviceName string analyticsRate float64 dsn string + tagFns map[string]func(scope *gorm.Scope) interface{} } // Option represents an option that can be passed to Register, Open or OpenDB. @@ -60,3 +63,14 @@ func WithAnalyticsRate(rate float64) Option { } } } + +// WithCustomTag will cause the given tagFn to be evaluated after executing +// a query and attach the result to the span tagged by the key. +func WithCustomTag(tag string, tagFn func(scope *gorm.Scope) interface{}) Option { + return func(cfg *config) { + if cfg.tagFns == nil { + cfg.tagFns = make(map[string]func(scope *gorm.Scope) interface{}) + } + cfg.tagFns[tag] = tagFn + } +} diff --git a/contrib/jinzhu/gorm/gorm.go b/contrib/jinzhu/gorm/gorm.go index 135cc43ee8..4362d9eda7 100644 --- a/contrib/jinzhu/gorm/gorm.go +++ b/contrib/jinzhu/gorm/gorm.go @@ -124,6 +124,11 @@ func after(scope *gorm.Scope, operationName string) { if !math.IsNaN(cfg.analyticsRate) { opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } + if cfg.tagFns != nil { + for key, tagFn := range cfg.tagFns { + opts = append(opts, tracer.Tag(key, tagFn(scope))) + } + } span, _ := tracer.StartSpanFromContext(ctx, operationName, opts...) span.Finish(tracer.WithError(scope.DB().Error)) diff --git a/contrib/jinzhu/gorm/gorm_test.go b/contrib/jinzhu/gorm/gorm_test.go index ef1a7f38f3..f4baed4353 100644 --- a/contrib/jinzhu/gorm/gorm_test.go +++ b/contrib/jinzhu/gorm/gorm_test.go @@ -303,3 +303,44 @@ func TestContext(t *testing.T) { assert.Equal(t, context.Background(), ctx) }) } + +func TestCustomTags(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + sqltrace.Register("postgres", &pq.Driver{}) + db, err := Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable", + WithCustomTag("custom_tag", func(scope *gorm.Scope) interface{} { + return scope.SQLVars[3] + }), + ) + if err != nil { + log.Fatal(err) + } + defer db.Close() + db.AutoMigrate(&Product{}) + + parentSpan, ctx := tracer.StartSpanFromContext(context.Background(), "http.request", + tracer.ServiceName("fake-http-server"), + tracer.SpanType(ext.SpanTypeWeb), + ) + + db = WithContext(ctx, db) + db.Create(&Product{Code: "L1212", Price: 1000}) + + parentSpan.Finish() + + spans := mt.FinishedSpans() + assert.True(len(spans) >= 3) + + // We deterministically expect the span to be the third last, + // followed by the underlying postgres DB trace and the above http.request span. + span := spans[len(spans)-3] + assert.Equal("gorm.create", span.OperationName()) + assert.Equal(ext.SpanTypeSQL, span.Tag(ext.SpanType)) + assert.Equal("L1212", span.Tag("custom_tag")) + assert.Equal( + `INSERT INTO "products" ("created_at","updated_at","deleted_at","code","price") VALUES ($1,$2,$3,$4,$5) RETURNING "products"."id"`, + span.Tag(ext.ResourceName)) +} diff --git a/contrib/jinzhu/gorm/option.go b/contrib/jinzhu/gorm/option.go index 7b799adbfe..094410f372 100644 --- a/contrib/jinzhu/gorm/option.go +++ b/contrib/jinzhu/gorm/option.go @@ -9,12 +9,15 @@ import ( "math" "gopkg.in/DataDog/dd-trace-go.v1/internal" + + "github.com/jinzhu/gorm" ) type config struct { serviceName string analyticsRate float64 dsn string + tagFns map[string]func(scope *gorm.Scope) interface{} } // Option represents an option that can be passed to Register, Open or OpenDB. @@ -60,3 +63,14 @@ func WithAnalyticsRate(rate float64) Option { } } } + +// WithCustomTag will cause the given tagFn to be evaluated after executing +// a query and attach the result to the span tagged by the key. +func WithCustomTag(tag string, tagFn func(scope *gorm.Scope) interface{}) Option { + return func(cfg *config) { + if cfg.tagFns == nil { + cfg.tagFns = make(map[string]func(scope *gorm.Scope) interface{}) + } + cfg.tagFns[tag] = tagFn + } +} From 7e9fc2b34cb46ef4f2b5639804211f34a51399a5 Mon Sep 17 00:00:00 2001 From: David Sanchez <838104+sanchda@users.noreply.github.com> Date: Thu, 10 Sep 2020 07:24:29 -0500 Subject: [PATCH 07/22] profiler: add API key check at initialization of Profiler object (#718) Ensure `newProfiler` fails when API key is invalid format (numbers and letters only, fixed length 32). --- profiler/options.go | 14 ++++++++++++++ profiler/options_test.go | 39 +++++++++++++++++++++++++++++++++------ profiler/profiler.go | 4 ++++ profiler/profiler_test.go | 10 ++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/profiler/options.go b/profiler/options.go index fba5afebe6..f63eea7ae1 100644 --- a/profiler/options.go +++ b/profiler/options.go @@ -14,6 +14,7 @@ import ( "runtime" "strings" "time" + "unicode" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" @@ -71,6 +72,19 @@ func urlForSite(site string) (string, error) { return u, err } +// isAPIKeyValid reports whether the given string is a structurally valid API key +func isAPIKeyValid(key string) bool { + if len(key) != 32 { + return false + } + for _, c := range key { + if c > unicode.MaxASCII || (!unicode.IsLower(c) && !unicode.IsNumber(c)) { + return false + } + } + return true +} + func (c *config) addProfileType(t ProfileType) { if c.types == nil { c.types = make(map[ProfileType]struct{}) diff --git a/profiler/options_test.go b/profiler/options_test.go index bcbdda4d8f..caa75a574a 100644 --- a/profiler/options_test.go +++ b/profiler/options_test.go @@ -9,6 +9,7 @@ import ( "net" "os" "path/filepath" + "strconv" "testing" "time" @@ -17,7 +18,32 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" ) +// testAPIKey is an example API key for validation purposes +const testAPIKey = "12345678901234567890123456789012" + func TestOptions(t *testing.T) { + t.Run("APIKeyChecks", func(t *testing.T) { + var apikeytests = []struct { + in string + out bool + }{ + {"", false}, // Fail, empty string + {"1234567890123456789012345678901", false}, // Fail, too short + {"123456789012345678901234567890123", false}, // Fail, too long + {"12345678901234567890123456789012", true}, // Pass, numeric only + {"abcdefabcdabcdefabcdefabcdefabcd", true}, // Pass, alpha only + {"abcdefabcdabcdef7890abcdef789012", true}, // Pass, alphanumeric + {"abcdefabcdabcdef7890Abcdef789012", false}, // Fail, contains an uppercase + {"abcdefabcdabcdef7890@bcdef789012", false}, // Fail, contains an ASCII symbol + {"abcdefabcdabcdef7890ábcdef789012", false}, // Fail, lowercase extended ASCII + {"abcdefabcdabcdef7890ábcdef78901", false}, // Fail, lowercase extended ASCII, conservative + } + + for i, tt := range apikeytests { + assert.Equal(t, tt.out, isAPIKeyValid(tt.in), strconv.Itoa(i)+" : "+tt.in) + } + }) + t.Run("WithAgentAddr", func(t *testing.T) { var cfg config WithAgentAddr("test:123")(&cfg) @@ -38,17 +64,18 @@ func TestOptions(t *testing.T) { t.Run("WithAPIKey", func(t *testing.T) { var cfg config - WithAPIKey("123")(&cfg) - assert.Equal(t, "123", cfg.apiKey) + WithAPIKey(testAPIKey)(&cfg) + assert.Equal(t, testAPIKey, cfg.apiKey) assert.Equal(t, cfg.apiURL, cfg.targetURL) }) t.Run("WithAPIKey/override", func(t *testing.T) { os.Setenv("DD_API_KEY", "apikey") defer os.Unsetenv("DD_API_KEY") + var testAPIKey = "12345678901234567890123456789012" var cfg config - WithAPIKey("123")(&cfg) - assert.Equal(t, "123", cfg.apiKey) + WithAPIKey(testAPIKey)(&cfg) + assert.Equal(t, testAPIKey, cfg.apiKey) }) t.Run("WithURL", func(t *testing.T) { @@ -179,10 +206,10 @@ func TestEnvVars(t *testing.T) { }) t.Run("DD_API_KEY", func(t *testing.T) { - os.Setenv("DD_API_KEY", "123") + os.Setenv("DD_API_KEY", testAPIKey) defer os.Unsetenv("DD_API_KEY") cfg := defaultConfig() - assert.Equal(t, "123", cfg.apiKey) + assert.Equal(t, testAPIKey, cfg.apiKey) }) t.Run("DD_SITE", func(t *testing.T) { diff --git a/profiler/profiler.go b/profiler/profiler.go index 9fb3ef1b59..332cfa96d1 100644 --- a/profiler/profiler.go +++ b/profiler/profiler.go @@ -6,6 +6,7 @@ package profiler import ( + "errors" "fmt" "os" "runtime" @@ -70,6 +71,9 @@ func newProfiler(opts ...Option) (*profiler, error) { opt(cfg) } if cfg.apiKey != "" { + if !isAPIKeyValid(cfg.apiKey) { + return nil, errors.New("API key has incorrect format") + } cfg.targetURL = cfg.apiURL } else { cfg.targetURL = cfg.agentURL diff --git a/profiler/profiler_test.go b/profiler/profiler_test.go index 863bb97514..d0bfdcf7ed 100644 --- a/profiler/profiler_test.go +++ b/profiler/profiler_test.go @@ -58,6 +58,16 @@ func TestStart(t *testing.T) { assert.NotEmpty(t, activeProfiler.cfg.hostname) mu.Unlock() }) + + t.Run("options/GoodAPIKey", func(t *testing.T) { + _, err := newProfiler(WithAPIKey("12345678901234567890123456789012")) + assert.Nil(t, err) + }) + + t.Run("options/BadAPIKey", func(t *testing.T) { + _, err := newProfiler(WithAPIKey("aaaa")) + assert.NotNil(t, err) + }) } func TestStartStopIdempotency(t *testing.T) { From bdf43a2e3817db0f617bb3dfb78d4d67437090e1 Mon Sep 17 00:00:00 2001 From: Carlo Alberto Ferraris Date: Fri, 11 Sep 2020 19:00:39 +0900 Subject: [PATCH 08/22] contrib: add support for cloud.google.com/go/pubsub (#721) --- .circleci/config.yml | 1 + .../go/pubsub.v1/example_test.go | 43 ++++ .../cloud.google.com/go/pubsub.v1/pubsub.go | 90 +++++++ .../go/pubsub.v1/pubsub_test.go | 229 ++++++++++++++++++ contrib/google.golang.org/api/api_test.go | 8 +- go.sum | 151 ++++++++++++ 6 files changed, 517 insertions(+), 5 deletions(-) create mode 100644 contrib/cloud.google.com/go/pubsub.v1/example_test.go create mode 100644 contrib/cloud.google.com/go/pubsub.v1/pubsub.go create mode 100644 contrib/cloud.google.com/go/pubsub.v1/pubsub_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index edd1d2a9d9..e9c1f43484 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -142,6 +142,7 @@ jobs: command: | go get k8s.io/client-go@v0.17.0 go get k8s.io/apimachinery@v0.17.0 + go get cloud.google.com/go/pubsub@v1.6.1 - run: name: Wait for MySQL diff --git a/contrib/cloud.google.com/go/pubsub.v1/example_test.go b/contrib/cloud.google.com/go/pubsub.v1/example_test.go new file mode 100644 index 0000000000..f142b6c6df --- /dev/null +++ b/contrib/cloud.google.com/go/pubsub.v1/example_test.go @@ -0,0 +1,43 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package pubsub_test + +import ( + "context" + "log" + + pubsubtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/cloud.google.com/go/pubsub.v1" + + "cloud.google.com/go/pubsub" +) + +func ExamplePublish() { + client, err := pubsub.NewClient(context.Background(), "project-id") + if err != nil { + log.Fatal(err) + } + + topic := client.Topic("topic") + _, err = pubsubtrace.Publish(context.Background(), topic, &pubsub.Message{Data: []byte("hello world!")}).Get(context.Background()) + if err != nil { + log.Fatal(err) + } +} + +func ExampleReceive() { + client, err := pubsub.NewClient(context.Background(), "project-id") + if err != nil { + log.Fatal(err) + } + + sub := client.Subscription("subscription") + err = sub.Receive(context.Background(), pubsubtrace.WrapReceiveHandler(sub, func(ctx context.Context, msg *pubsub.Message) { + // TODO: Handle message. + })) + if err != nil { + log.Fatal(err) + } +} diff --git a/contrib/cloud.google.com/go/pubsub.v1/pubsub.go b/contrib/cloud.google.com/go/pubsub.v1/pubsub.go new file mode 100644 index 0000000000..b892363708 --- /dev/null +++ b/contrib/cloud.google.com/go/pubsub.v1/pubsub.go @@ -0,0 +1,90 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +// Package pubsub provides functions to trace the cloud.google.com/pubsub/go package. +package pubsub + +import ( + "context" + "sync" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + + "cloud.google.com/go/pubsub" +) + +// Publish publishes a message on the specified topic and returns a PublishResult. +// This function is functionally equivalent to t.Publish(ctx, msg), but it also starts a publish +// span and it ensures that the tracing metadata is propagated as attributes attached to +// the published message. +// It is required to call (*PublishResult).Get(ctx) on the value returned by Publish to complete +// the span. +func Publish(ctx context.Context, t *pubsub.Topic, msg *pubsub.Message) *PublishResult { + span, ctx := tracer.StartSpanFromContext( + ctx, + "pubsub.publish", + tracer.ResourceName(t.String()), + tracer.SpanType(ext.SpanTypeMessageProducer), + tracer.Tag("message_size", len(msg.Data)), + tracer.Tag("ordering_key", msg.OrderingKey), + ) + if msg.Attributes == nil { + msg.Attributes = make(map[string]string) + } + if err := tracer.Inject(span.Context(), tracer.TextMapCarrier(msg.Attributes)); err != nil { + log.Debug("contrib/cloud.google.com/go/pubsub.v1/: failed injecting tracing attributes: %v", err) + } + span.SetTag("num_attributes", len(msg.Attributes)) + return &PublishResult{ + PublishResult: t.Publish(ctx, msg), + span: span, + } +} + +// PublishResult wraps *pubsub.PublishResult +type PublishResult struct { + *pubsub.PublishResult + once sync.Once + span tracer.Span +} + +// Get wraps (pubsub.PublishResult).Get(ctx). When this function returns the publish +// span created in Publish is completed. +func (r *PublishResult) Get(ctx context.Context) (string, error) { + serverID, err := r.PublishResult.Get(ctx) + r.once.Do(func() { + r.span.SetTag("server_id", serverID) + r.span.Finish(tracer.WithError(err)) + }) + return serverID, err +} + +// WrapReceiveHandler returns a receive handler that wraps the supplied handler, +// extracts any tracing metadata attached to the received message, and starts a +// receive span. +func WrapReceiveHandler(s *pubsub.Subscription, f func(context.Context, *pubsub.Message)) func(context.Context, *pubsub.Message) { + return func(ctx context.Context, msg *pubsub.Message) { + parentSpanCtx, _ := tracer.Extract(tracer.TextMapCarrier(msg.Attributes)) + span, ctx := tracer.StartSpanFromContext( + ctx, + "pubsub.receive", + tracer.ResourceName(s.String()), + tracer.SpanType(ext.SpanTypeMessageConsumer), + tracer.Tag("message_size", len(msg.Data)), + tracer.Tag("num_attributes", len(msg.Attributes)), + tracer.Tag("ordering_key", msg.OrderingKey), + tracer.Tag("message_id", msg.ID), + tracer.Tag("publish_time", msg.PublishTime.String()), + tracer.ChildOf(parentSpanCtx), + ) + if msg.DeliveryAttempt != nil { + span.SetTag("delivery_attempt", *msg.DeliveryAttempt) + } + defer span.Finish() + f(ctx, msg) + } +} diff --git a/contrib/cloud.google.com/go/pubsub.v1/pubsub_test.go b/contrib/cloud.google.com/go/pubsub.v1/pubsub_test.go new file mode 100644 index 0000000000..1b120b34ca --- /dev/null +++ b/contrib/cloud.google.com/go/pubsub.v1/pubsub_test.go @@ -0,0 +1,229 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package pubsub + +import ( + "context" + "testing" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "cloud.google.com/go/pubsub" + "cloud.google.com/go/pubsub/pstest" + "github.com/stretchr/testify/assert" + "google.golang.org/api/option" + "google.golang.org/grpc" +) + +func TestPropagation(t *testing.T) { + assert := assert.New(t) + ctx, topic, sub, mt, cleanup := setup(t) + defer cleanup() + + // Publisher + span, pctx := tracer.StartSpanFromContext(ctx, "propagation-test", tracer.WithSpanID(42)) // set the root trace ID + srvID, err := Publish(pctx, topic, &pubsub.Message{Data: []byte("hello"), OrderingKey: "xxx"}).Get(pctx) + assert.NoError(err) + span.Finish() + + // Subscriber + var ( + msgID string + spanID uint64 + pubTime string + called bool + ) + err = sub.Receive(ctx, WrapReceiveHandler(sub, func(ctx context.Context, msg *pubsub.Message) { + assert.False(called, "callback called twice") + assert.Equal(msg.Data, []byte("hello"), "wrong payload") + span, ok := tracer.SpanFromContext(ctx) + assert.True(ok, "no span") + assert.Equal(uint64(42), span.Context().TraceID(), "wrong trace id") // gist of the test: the trace ID must be the same as the root trace ID set above + msgID = msg.ID + spanID = span.Context().SpanID() + pubTime = msg.PublishTime.String() + msg.Ack() + called = true + })) + assert.True(called, "callback not called") + assert.NoError(err) + + spans := mt.FinishedSpans() + assert.Len(spans, 3, "wrong number of spans") + assert.Equal("pubsub.publish", spans[0].OperationName()) + assert.Equal("propagation-test", spans[1].OperationName()) + assert.Equal("pubsub.receive", spans[2].OperationName()) + + assert.Equal(spans[1].SpanID(), spans[0].ParentID()) + assert.Equal(uint64(42), spans[0].TraceID()) + assert.Equal(map[string]interface{}{ + "message_size": 5, + "num_attributes": 2, // 2 tracing attributes + "ordering_key": "xxx", + ext.ResourceName: "projects/project/topics/topic", + ext.SpanType: ext.SpanTypeMessageProducer, + "server_id": srvID, + ext.ServiceName: nil, + }, spans[0].Tags()) + + assert.Equal(spans[0].SpanID(), spans[2].ParentID()) + assert.Equal(uint64(42), spans[2].TraceID()) + assert.Equal(spanID, spans[2].SpanID()) + assert.Equal(map[string]interface{}{ + "message_size": 5, + "num_attributes": 2, + "ordering_key": "xxx", + ext.ResourceName: "projects/project/subscriptions/subscription", + ext.SpanType: ext.SpanTypeMessageConsumer, + "message_id": msgID, + "publish_time": pubTime, + }, spans[2].Tags()) +} + +func TestPropagationNoParentSpan(t *testing.T) { + assert := assert.New(t) + ctx, topic, sub, mt, cleanup := setup(t) + defer cleanup() + + // Publisher + // no parent span + srvID, err := Publish(ctx, topic, &pubsub.Message{Data: []byte("hello"), OrderingKey: "xxx"}).Get(ctx) + assert.NoError(err) + + // Subscriber + var ( + msgID string + spanID uint64 + traceID uint64 + pubTime string + called bool + ) + err = sub.Receive(ctx, WrapReceiveHandler(sub, func(ctx context.Context, msg *pubsub.Message) { + assert.False(called, "callback called twice") + assert.Equal(msg.Data, []byte("hello"), "wrong payload") + span, ok := tracer.SpanFromContext(ctx) + assert.True(ok, "no span") + msgID = msg.ID + spanID = span.Context().SpanID() + traceID = span.Context().TraceID() + pubTime = msg.PublishTime.String() + msg.Ack() + called = true + })) + assert.True(called, "callback not called") + assert.NoError(err) + + spans := mt.FinishedSpans() + assert.Len(spans, 2, "wrong number of spans") + assert.Equal("pubsub.publish", spans[0].OperationName()) + assert.Equal("pubsub.receive", spans[1].OperationName()) + + assert.Equal(spans[0].TraceID(), spans[0].SpanID()) + assert.Equal(traceID, spans[0].TraceID()) + assert.Equal(map[string]interface{}{ + "message_size": 5, + "num_attributes": 2, + "ordering_key": "xxx", + ext.ResourceName: "projects/project/topics/topic", + ext.SpanType: ext.SpanTypeMessageProducer, + "server_id": srvID, + }, spans[0].Tags()) + + assert.Equal(spans[0].SpanID(), spans[1].ParentID()) + assert.Equal(traceID, spans[1].TraceID()) + assert.Equal(spanID, spans[1].SpanID()) + assert.Equal(map[string]interface{}{ + "message_size": 5, + "num_attributes": 2, + "ordering_key": "xxx", + ext.ResourceName: "projects/project/subscriptions/subscription", + ext.SpanType: ext.SpanTypeMessageConsumer, + "message_id": msgID, + "publish_time": pubTime, + }, spans[1].Tags()) +} + +func TestPropagationNoPubsliherSpan(t *testing.T) { + assert := assert.New(t) + ctx, topic, sub, mt, cleanup := setup(t) + defer cleanup() + + // Publisher + // no tracing on publisher side + _, err := topic.Publish(ctx, &pubsub.Message{Data: []byte("hello"), OrderingKey: "xxx"}).Get(ctx) + assert.NoError(err) + + // Subscriber + var ( + msgID string + spanID uint64 + traceID uint64 + pubTime string + called bool + ) + err = sub.Receive(ctx, WrapReceiveHandler(sub, func(ctx context.Context, msg *pubsub.Message) { + assert.False(called, "callback called twice") + assert.Equal(msg.Data, []byte("hello"), "wrong payload") + span, ok := tracer.SpanFromContext(ctx) + assert.True(ok, "no span") + msgID = msg.ID + spanID = span.Context().SpanID() + traceID = span.Context().TraceID() + pubTime = msg.PublishTime.String() + msg.Ack() + called = true + })) + assert.True(called, "callback not called") + assert.NoError(err) + + spans := mt.FinishedSpans() + assert.Len(spans, 1, "wrong number of spans") + assert.Equal("pubsub.receive", spans[0].OperationName()) + + assert.Equal(traceID, spans[0].TraceID()) + assert.Equal(spanID, spans[0].SpanID()) + assert.Equal(map[string]interface{}{ + "message_size": 5, + "num_attributes": 0, // no attributes, since no publish middleware sent them + "ordering_key": "xxx", + ext.ResourceName: "projects/project/subscriptions/subscription", + ext.SpanType: ext.SpanTypeMessageConsumer, + "message_id": msgID, + "publish_time": pubTime, + }, spans[0].Tags()) +} + +func setup(t *testing.T) (context.Context, *pubsub.Topic, *pubsub.Subscription, mocktracer.Tracer, func()) { + assert := assert.New(t) + mt := mocktracer.Start() + + srv := pstest.NewServer() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure()) + assert.NoError(err) + client, err := pubsub.NewClient(ctx, "project", option.WithGRPCConn(conn)) + assert.NoError(err) + _, err = client.CreateTopic(ctx, "topic") + assert.NoError(err) + topic := client.Topic("topic") + topic.EnableMessageOrdering = true + _, err = client.CreateSubscription(ctx, "subscription", pubsub.SubscriptionConfig{ + Topic: topic, + }) + assert.NoError(err) + sub := client.Subscription("subscription") + + return ctx, topic, sub, mt, func() { + // use t.Cleanup() once go 1.14 is available + conn.Close() + cancel() + srv.Close() + mt.Stop() + } +} diff --git a/contrib/google.golang.org/api/api_test.go b/contrib/google.golang.org/api/api_test.go index 1beb600ed3..fab1a55ea8 100644 --- a/contrib/google.golang.org/api/api_test.go +++ b/contrib/google.golang.org/api/api_test.go @@ -69,9 +69,7 @@ func TestCivicInfo(t *testing.T) { Transport: WrapRoundTripper(badRequestTransport), }) assert.NoError(t, err) - svc.Representatives. - RepresentativeInfoByAddress(&civicinfo.RepresentativeInfoRequest{}). - Do() + svc.Representatives.RepresentativeInfoByAddress().Do() spans := mt.FinishedSpans() assert.Len(t, spans, 1) @@ -79,8 +77,8 @@ func TestCivicInfo(t *testing.T) { s0 := spans[0] assert.Equal(t, "http.request", s0.OperationName()) assert.Equal(t, "http", s0.Tag(ext.SpanType)) - assert.Equal(t, "google.civicinfo", s0.Tag(ext.ServiceName)) - assert.Equal(t, "civicinfo.representatives.representativeInfoByAddress", s0.Tag(ext.ResourceName)) + assert.Equal(t, "google", s0.Tag(ext.ServiceName)) + assert.Equal(t, "GET civicinfo.googleapis.com", s0.Tag(ext.ResourceName)) assert.Equal(t, "400", s0.Tag(ext.HTTPCode)) assert.Equal(t, "GET", s0.Tag(ext.HTTPMethod)) assert.Equal(t, "/civicinfo/v2/representatives", s0.Tag(ext.HTTPURL)) diff --git a/go.sum b/go.sum index 1696a5ff97..9dc245a3a1 100644 --- a/go.sum +++ b/go.sum @@ -10,19 +10,34 @@ cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.61.0 h1:NLQf5e1OMspfNT1RAHOB3ublr1TW3YTXO8OiWwVjK2U= +cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.6.1 h1:lhCQrTgu7f5SjWm5yJO0geSsPORQ2OAD+Eq1AMyBW8Y= +cloud.google.com/go/pubsub v1.6.1/go.mod h1:kvW9rcn9OLEx6eTIzMBbWbpB8YsK3vu9jxgPolVz+p4= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -30,6 +45,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.7.1+incompatible h1:HmA9qHVrHIAqpSvoCYJ+c6qst0lgqEhNW6/KwfkHbS8= github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v4.0.0+incompatible h1:Dq8Dr+4sV1gBO1sHDWdW+4G+PdsA+YSJOK925MxrrCY= +github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= @@ -39,15 +56,22 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWso github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4= github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= +github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= +github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= +github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.29.11/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= github.com/aws/aws-sdk-go v1.31.9 h1:n+b34ydVfgC30j0Qm69yaapmjejQPW2BoDBX7Uy/tLI= github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.31.10 h1:33jOMifUSdOP9pvNEOj+PGwljzunc8bJvKKNF/JuGzo= github.com/aws/aws-sdk-go v1.31.10/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= @@ -113,6 +137,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= @@ -175,18 +200,22 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -206,17 +235,28 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -320,11 +360,13 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5 h1:lrdPtrORjGv1HbbEvKWDUAy97mPpFm4B8hp77tcCUJY= github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -340,6 +382,7 @@ github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -365,10 +408,12 @@ github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -379,6 +424,7 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= @@ -418,6 +464,7 @@ github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= @@ -437,6 +484,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4 v2.4.1+incompatible h1:mFe7ttWaflA46Mhqh+jUfjp2qTbPYxLB2/OyBppH9dg= github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -446,6 +494,7 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 h1:dY6ETXrvDG7Sa4vE8ZQG4yqWg6UnOcbqTAahkV813vQ= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -455,12 +504,15 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -478,6 +530,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= @@ -498,6 +552,12 @@ github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/twitchtv/twirp v5.11.0+incompatible h1:hO4u9Yep59kTN1JhYjM+cpELT2mqjTf+xMi21UOPXQc= github.com/twitchtv/twirp v5.11.0+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= @@ -515,6 +575,8 @@ github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHM github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5 h1:mXV20Aj/BdWrlVzIn1kXFa+Tq62INlUi0cFFlztTaK0= github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= @@ -528,11 +590,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -543,6 +608,7 @@ golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -564,6 +630,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -573,6 +640,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -587,6 +656,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -595,9 +665,19 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -614,6 +694,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -650,12 +732,20 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -664,11 +754,15 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -705,12 +799,28 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200725200936-102e7d357031 h1:VtIxiVHWPhnny2ZTi4f9/2diZKqyLaq3FUTuud5+khA= +golang.org/x/tools v0.0.0-20200725200936-102e7d357031/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -720,9 +830,18 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08= google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo= +google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -730,6 +849,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -750,8 +871,24 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 h1:MRHtG0U6SnaUb+s+LhNE1qt1FQ1wlhqr5E4usBKC0uA= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d h1:HJaAqDnKreMkv+AQyf1Mcw0jEmL9kKBNL07RDJu1N/k= +google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103 h1:z46CEPU+LlO0kGGwrH8h5epkkJhRZbAHYWOWD9JhLPI= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -767,13 +904,24 @@ google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -815,6 +963,8 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -822,6 +972,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.18.3 h1:2AJaUQdgUZLoDZHrun21PW2Nx9+ll6cUzvn3IKhSIn0= k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA= k8s.io/apimachinery v0.18.3 h1:pOGcbVAhxADgUYnjS08EFXs9QMl8qaH5U4fr5LGUrSk= From 6b29b2aa0a7dbe57d5d3afdc35b416f4f0e854d5 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Tue, 15 Sep 2020 09:04:36 -0500 Subject: [PATCH 09/22] ddtrace/tracer: add support for lambda tracing (#702) This commit abstracts the handling of finished traces into traceWriters, and includes 2 traceWriter implementations. One implements the usual behavior, sending traces to the trace agent. The other supports sending traces in a serverless environment by writing completed traces to the log. Fixes #701 --- ddtrace/ddtrace.go | 3 +- ddtrace/tracer/option.go | 15 ++ ddtrace/tracer/sampler_test.go | 5 +- ddtrace/tracer/span_test.go | 4 +- ddtrace/tracer/tracer.go | 87 +++------- ddtrace/tracer/tracer_test.go | 16 +- ddtrace/tracer/writer.go | 305 +++++++++++++++++++++++++++++++++ ddtrace/tracer/writer_test.go | 298 ++++++++++++++++++++++++++++++++ internal/log/log.go | 1 + 9 files changed, 656 insertions(+), 78 deletions(-) create mode 100644 ddtrace/tracer/writer.go create mode 100644 ddtrace/tracer/writer_test.go diff --git a/ddtrace/ddtrace.go b/ddtrace/ddtrace.go index 6dfde7ebc0..c33104b27f 100644 --- a/ddtrace/ddtrace.go +++ b/ddtrace/ddtrace.go @@ -30,8 +30,7 @@ type Tracer interface { // Inject injects a span context into the given carrier. Inject(context SpanContext, carrier interface{}) error - // Stop stops the active tracer and sets the global tracer to a no-op. Calls to - // Stop should be idempotent. + // Stop stops the tracer. Calls to Stop should be idempotent. Stop() } diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index f0cc0161fd..d5c52259ba 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -30,6 +30,9 @@ type config struct { // debug, when true, writes details to logs. debug bool + // lambda, when true, enables the lambda trace writer + logToStdout bool + // logStartup, when true, causes various startup info to be written // when the tracer starts. logStartup bool @@ -145,6 +148,11 @@ func newConfig(opts ...StartOption) *config { } } } + if _, ok := os.LookupEnv("AWS_LAMBDA_FUNCTION_NAME"); ok { + // AWS_LAMBDA_FUNCTION_NAME being set indicates that we're running in an AWS Lambda environment. + // See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html + c.logToStdout = true + } c.logStartup = internal.BoolEnv("DD_TRACE_STARTUP_LOGS", true) c.runtimeMetrics = internal.BoolEnv("DD_RUNTIME_METRICS_ENABLED", false) c.debug = internal.BoolEnv("DD_TRACE_DEBUG", false) @@ -248,6 +256,13 @@ func WithDebugMode(enabled bool) StartOption { } } +// WithLambdaMode enables lambda mode on the tracer, for use with AWS Lambda. +func WithLambdaMode(enabled bool) StartOption { + return func(c *config) { + c.logToStdout = enabled + } +} + // WithPropagator sets an alternative propagator to be used by the tracer. func WithPropagator(p Propagator) StartOption { return func(c *config) { diff --git a/ddtrace/tracer/sampler_test.go b/ddtrace/tracer/sampler_test.go index 009d6c3acc..615be254bb 100644 --- a/ddtrace/tracer/sampler_test.go +++ b/ddtrace/tracer/sampler_test.go @@ -498,7 +498,6 @@ func BenchmarkRulesSampler(b *testing.B) { benchmarkStartSpan := func(b *testing.B, t *tracer) { internal.SetGlobalTracer(t) defer func() { - close(t.stop) internal.SetGlobalTracer(&internal.NoopTracer{}) }() t.prioritySampling.readRatesJSON(ioutil.NopCloser(strings.NewReader( @@ -527,8 +526,8 @@ func BenchmarkRulesSampler(b *testing.B) { spans[j].Finish() } d := 0 - for len(t.payloadChan) > 0 { - <-t.payloadChan + for len(t.out) > 0 { + <-t.out d++ } } diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 7927aff02d..8f9d9d0cd5 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -84,7 +84,7 @@ func TestSpanFinishTwice(t *testing.T) { tracer, _, _, stop := startTestTracer(t) defer stop() - assert.Equal(tracer.payload.itemCount(), 0) + assert.Equal(tracer.traceWriter.(*agentTraceWriter).payload.itemCount(), 0) // the finish must be idempotent span := tracer.newRootSpan("pylons.request", "pylons", "/") @@ -367,7 +367,7 @@ func TestSpanModifyWhileFlushing(t *testing.T) { case <-done: return default: - tracer.flush() + tracer.traceWriter.flush() time.Sleep(10 * time.Millisecond) } } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 5a1fb88001..973ed4b0c7 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -29,13 +29,13 @@ var _ ddtrace.Tracer = (*tracer)(nil) // queues to be processed by the payload encoder. type tracer struct { *config - *payload - // payloadChan receives traces to be added to the payload. - payloadChan chan []*span + // traceWriter is responsible for sending finished traces to their + // destination, such as the Trace Agent or Datadog Forwarder. + traceWriter traceWriter - // climit limits the number of concurrent outgoing connections - climit chan struct{} + // out receives traces to be added to the payload. + out chan []*span // stop causes the tracer to shut down when closed. stop chan struct{} @@ -141,14 +141,20 @@ func newUnstartedTracer(opts ...StartOption) *tracer { if envRules != nil { c.samplingRules = envRules } + sampler := newPrioritySampler() + var writer traceWriter + if c.logToStdout { + writer = newLogTraceWriter(c) + } else { + writer = newAgentTraceWriter(c, sampler) + } return &tracer{ config: c, - payload: newPayload(), - payloadChan: make(chan []*span, payloadQueueSize), + traceWriter: writer, + out: make(chan []*span, payloadQueueSize), stop: make(chan struct{}), rulesSampling: newRulesSampler(c.samplingRules), - climit: make(chan struct{}, concurrentConnectionLimit), - prioritySampling: newPrioritySampler(), + prioritySampling: sampler, pid: strconv.Itoa(os.Getpid()), } } @@ -188,16 +194,14 @@ func newTracer(opts ...StartOption) *tracer { // worker receives finished traces to be added into the payload, as well // as periodically flushes traces to the transport. func (t *tracer) worker(tick <-chan time.Time) { - defer t.config.statsd.Close() - for { select { - case trace := <-t.payloadChan: - t.pushPayload(trace) + case trace := <-t.out: + t.traceWriter.add(trace) case <-tick: t.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:scheduled"}, 1) - t.flush() + t.traceWriter.flush() case <-t.stop: loop: @@ -205,15 +209,12 @@ func (t *tracer) worker(tick <-chan time.Time) { // before the final flush to ensure no traces are lost (see #526) for { select { - case trace := <-t.payloadChan: - t.pushPayload(trace) + case trace := <-t.out: + t.traceWriter.add(trace) default: break loop } } - t.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:shutdown"}, 1) - t.flush() - t.config.statsd.Incr("datadog.tracer.stopped", nil, 1) return } } @@ -226,7 +227,7 @@ func (t *tracer) pushTrace(trace []*span) { default: } select { - case t.payloadChan <- trace: + case t.out <- trace: default: log.Error("payload queue full, dropping %d traces", len(trace)) } @@ -322,8 +323,11 @@ func (t *tracer) StartSpan(operationName string, options ...ddtrace.StartSpanOpt func (t *tracer) Stop() { t.stopOnce.Do(func() { close(t.stop) + t.config.statsd.Incr("datadog.tracer.stopped", nil, 1) }) t.wg.Wait() + t.traceWriter.stop() + t.config.statsd.Close() } // Inject uses the configured or default TextMap Propagator. @@ -336,49 +340,6 @@ func (t *tracer) Extract(carrier interface{}) (ddtrace.SpanContext, error) { return t.config.propagator.Extract(carrier) } -// flush will push any currently buffered traces to the server. -func (t *tracer) flush() { - if t.payload.itemCount() == 0 { - return - } - t.wg.Add(1) - t.climit <- struct{}{} - go func(p *payload) { - defer func(start time.Time) { - <-t.climit - t.wg.Done() - t.config.statsd.Timing("datadog.tracer.flush_duration", time.Since(start), nil, 1) - }(time.Now()) - size, count := p.size(), p.itemCount() - log.Debug("Sending payload: size: %d traces: %d\n", size, count) - rc, err := t.config.transport.send(p) - if err != nil { - t.config.statsd.Count("datadog.tracer.traces_dropped", int64(count), []string{"reason:send_failed"}, 1) - log.Error("lost %d traces: %v", count, err) - } else { - t.config.statsd.Count("datadog.tracer.flush_bytes", int64(size), nil, 1) - t.config.statsd.Count("datadog.tracer.flush_traces", int64(count), nil, 1) - if err := t.prioritySampling.readRatesJSON(rc); err != nil { - t.config.statsd.Incr("datadog.tracer.decode_error", nil, 1) - } - } - }(t.payload) - t.payload = newPayload() -} - -// pushPayload pushes the trace onto the payload. If the payload becomes -// larger than the threshold as a result, it sends a flush request. -func (t *tracer) pushPayload(trace []*span) { - if err := t.payload.push(trace); err != nil { - t.config.statsd.Incr("datadog.tracer.traces_dropped", []string{"reason:encoding_error"}, 1) - log.Error("error encoding msgpack: %v", err) - } - if t.payload.size() > payloadSizeLimit { - t.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:size"}, 1) - t.flush() - } -} - // sampleRateMetricKey is the metric key holding the applied sample rate. Has to be the same as the Agent. const sampleRateMetricKey = "_sample_rate" diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index 78faa8903c..ff4ee2c92b 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -51,7 +51,7 @@ loop: case <-timeout: tst.Fatalf("timed out waiting for payload to contain %d", n) default: - if t.payload.itemCount() == n { + if t.traceWriter.(*agentTraceWriter).payload.itemCount() == n { break loop } time.Sleep(10 * time.Millisecond) @@ -604,7 +604,7 @@ func TestTracerEdgeSampler(t *testing.T) { span1.Finish() } - assert.Equal(tracer0.payload.itemCount(), 0) + assert.Equal(tracer0.traceWriter.(*agentTraceWriter).payload.itemCount(), 0) tracer1.awaitPayload(t, count) } @@ -915,11 +915,11 @@ func TestPushPayload(t *testing.T) { s.Meta["key"] = strings.Repeat("X", payloadSizeLimit/2+10) // half payload size reached - tracer.pushPayload([]*span{s}) + tracer.pushTrace([]*span{s}) tracer.awaitPayload(t, 1) // payload size exceeded - tracer.pushPayload([]*span{s}) + tracer.pushTrace([]*span{s}) flush(2) } @@ -943,18 +943,18 @@ func TestPushTrace(t *testing.T) { } tracer.pushTrace(trace) - assert.Len(tracer.payloadChan, 1) + assert.Len(tracer.out, 1) - t0 := <-tracer.payloadChan + t0 := <-tracer.out assert.Equal(trace, t0) many := payloadQueueSize + 2 for i := 0; i < many; i++ { tracer.pushTrace(make([]*span, i)) } - assert.Len(tracer.payloadChan, payloadQueueSize) + assert.Len(tracer.out, payloadQueueSize) log.Flush() - assert.True(len(tp.Lines()) >= 2) + assert.True(len(tp.Lines()) >= 1) } func TestTracerFlush(t *testing.T) { diff --git a/ddtrace/tracer/writer.go b/ddtrace/tracer/writer.go new file mode 100644 index 0000000000..c57b03f195 --- /dev/null +++ b/ddtrace/tracer/writer.go @@ -0,0 +1,305 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "bytes" + "errors" + "io" + "math" + "os" + "strconv" + "sync" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +type traceWriter interface { + // add adds traces to be sent by the writer. + add([]*span) + + // flush causes the writer to send any buffered traces. + flush() + + // stop gracefully shuts down the writer. + stop() +} + +type agentTraceWriter struct { + // config holds the tracer configuration + config *config + + // payload encodes and buffers traces in msgpack format + payload *payload + + // climit limits the number of concurrent outgoing connections + climit chan struct{} + + // wg waits for all uploads to finish + wg sync.WaitGroup + + // prioritySampling is the prioritySampler into which agentTraceWriter will + // read sampling rates sent by the agent + prioritySampling *prioritySampler +} + +func newAgentTraceWriter(c *config, s *prioritySampler) *agentTraceWriter { + return &agentTraceWriter{ + config: c, + payload: newPayload(), + climit: make(chan struct{}, concurrentConnectionLimit), + prioritySampling: s, + } +} + +func (h *agentTraceWriter) add(trace []*span) { + if err := h.payload.push(trace); err != nil { + h.config.statsd.Incr("datadog.tracer.traces_dropped", []string{"reason:encoding_error"}, 1) + log.Error("error encoding msgpack: %v", err) + } + if h.payload.size() > payloadSizeLimit { + h.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:size"}, 1) + h.flush() + } +} + +func (h *agentTraceWriter) stop() { + h.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:shutdown"}, 1) + h.flush() + h.wg.Wait() +} + +// flush will push any currently buffered traces to the server. +func (h *agentTraceWriter) flush() { + if h.payload.itemCount() == 0 { + return + } + h.wg.Add(1) + h.climit <- struct{}{} + go func(p *payload) { + defer func(start time.Time) { + <-h.climit + h.wg.Done() + h.config.statsd.Timing("datadog.tracer.flush_duration", time.Since(start), nil, 1) + }(time.Now()) + size, count := p.size(), p.itemCount() + log.Debug("Sending payload: size: %d traces: %d\n", size, count) + rc, err := h.config.transport.send(p) + if err != nil { + h.config.statsd.Count("datadog.tracer.traces_dropped", int64(count), []string{"reason:send_failed"}, 1) + log.Error("lost %d traces: %v", count, err) + } else { + h.config.statsd.Count("datadog.tracer.flush_bytes", int64(size), nil, 1) + h.config.statsd.Count("datadog.tracer.flush_traces", int64(count), nil, 1) + if err := h.prioritySampling.readRatesJSON(rc); err != nil { + h.config.statsd.Incr("datadog.tracer.decode_error", nil, 1) + } + } + }(h.payload) + h.payload = newPayload() +} + +// logTraceWriter encodes traces into a format understood by the Datadog Forwarder +// (https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring) +// and writes them to os.Stdout. This is used to send traces from an AWS Lambda environment. +type logTraceWriter struct { + config *config + buf bytes.Buffer + hasTraces bool + w io.Writer +} + +func newLogTraceWriter(c *config) *logTraceWriter { + w := &logTraceWriter{ + config: c, + w: os.Stdout, + } + w.resetBuffer() + return w +} + +const ( + // maxFloatLength is the maximum length that a string encoded by encodeFloat will be. + maxFloatLength = 24 + + // logBufferSuffix is the final string that the trace writer has to append to a buffer to close + // the JSON. + logBufferSuffix = "]}\n" + + // logBufferLimit is the maximum size log line allowed by cloudwatch + logBufferLimit = 256 * 1024 +) + +func (h *logTraceWriter) resetBuffer() { + h.buf.Reset() + h.buf.WriteString(`{"traces": [`) + h.hasTraces = false +} + +// encodeFloat correctly encodes float64 into the JSON format followed by ES6. +// This code is reworked from Go's encoding/json package +// (https://github.com/golang/go/blob/go1.15/src/encoding/json/encode.go#L573) +// +// One important departure from encoding/json is that infinities and nans are encoded +// as null rather than signalling an error. +func encodeFloat(p []byte, f float64) []byte { + if math.IsInf(f, 0) || math.IsNaN(f) { + return append(p, "null"...) + } + abs := math.Abs(f) + if abs != 0 && (abs < 1e-6 || abs >= 1e21) { + p = strconv.AppendFloat(p, f, 'e', -1, 64) + // clean up e-09 to e-9 + n := len(p) + if n >= 4 && p[n-4] == 'e' && p[n-3] == '-' && p[n-2] == '0' { + p[n-2] = p[n-1] + p = p[:n-1] + } + } else { + p = strconv.AppendFloat(p, f, 'f', -1, 64) + } + return p +} + +func (h *logTraceWriter) encodeSpan(s *span) { + var scratch [maxFloatLength]byte + h.buf.WriteString(`{"trace_id":"`) + h.buf.Write(strconv.AppendUint(scratch[:0], uint64(s.TraceID), 16)) + h.buf.WriteString(`","span_id":"`) + h.buf.Write(strconv.AppendUint(scratch[:0], uint64(s.SpanID), 16)) + h.buf.WriteString(`","parent_id":"`) + h.buf.Write(strconv.AppendUint(scratch[:0], uint64(s.ParentID), 16)) + h.buf.WriteString(`","name":"`) + h.buf.WriteString(s.Name) + h.buf.WriteString(`","resource":"`) + h.buf.WriteString(s.Resource) + h.buf.WriteString(`","error":`) + h.buf.Write(strconv.AppendInt(scratch[:0], int64(s.Error), 10)) + h.buf.WriteString(`,"meta":{`) + first := true + for k, v := range s.Meta { + if first { + h.buf.WriteByte('"') + first = false + } else { + h.buf.WriteString(`,"`) + } + h.buf.WriteString(k) + h.buf.WriteString(`":"`) + h.buf.WriteString(v) + h.buf.WriteString(`"`) + } + h.buf.WriteString(`},"metrics":{`) + first = true + for k, v := range s.Metrics { + if math.IsNaN(v) || math.IsInf(v, 0) { + // The trace forwarder does not support infinity or nan, so we do not send metrics with those values. + continue + } + if first { + h.buf.WriteByte('"') + first = false + } else { + h.buf.WriteString(`,"`) + } + h.buf.WriteString(k) + h.buf.WriteString(`":`) + h.buf.Write(encodeFloat(scratch[:0], v)) + } + h.buf.WriteString(`},"start":`) + h.buf.Write(strconv.AppendInt(scratch[:0], s.Start, 10)) + h.buf.WriteString(`,"duration":`) + h.buf.Write(strconv.AppendInt(scratch[:0], s.Duration, 10)) + h.buf.WriteString(`,"service":"`) + h.buf.WriteString(s.Service) + h.buf.WriteString(`"}`) +} + +type encodingError struct { + cause error + dropReason string +} + +// writeTrace makes an effort to write the trace into the current buffer. It returns +// the number of spans (n) that it wrote and an error (err), if one occurred. +// n may be less than len(trace), meaning that only the first n spans of the trace +// fit into the current buffer. Once the buffer is flushed, the remaining spans +// from the trace can be retried. +// An error, if one is returned, indicates that a span in the trace is too large +// to fit in one buffer, and the trace cannot be written. +func (h *logTraceWriter) writeTrace(trace []*span) (n int, err *encodingError) { + startn := h.buf.Len() + if !h.hasTraces { + h.buf.WriteByte('[') + } else { + h.buf.WriteString(", [") + } + written := 0 + for i, s := range trace { + n := h.buf.Len() + if i > 0 { + h.buf.WriteByte(',') + } + h.encodeSpan(s) + if h.buf.Len() > logBufferLimit-len(logBufferSuffix) { + // This span is too big to fit in the current buffer. + if i == 0 { + // This was the first span in this trace. This means we should truncate + // everything we wrote in writeTrace + h.buf.Truncate(startn) + if !h.hasTraces { + // This is the first span of the first trace in the buffer and it's too big. + // We will never be able to send this trace, so we will drop it. + return 0, &encodingError{cause: errors.New("span too large for buffer"), dropReason: "trace_too_large"} + } + return 0, nil + } + // This span was too big, but it might fit in the next buffer. + // We can finish this trace and try again with an empty buffer (see *logTaceWriter.add) + h.buf.Truncate(n) + break + } + written++ + } + h.buf.WriteByte(']') + h.hasTraces = true + return written, nil +} + +// add adds a trace to the writer's buffer. +func (h *logTraceWriter) add(trace []*span) { + // Try adding traces to the buffer until we flush them all or encounter an error. + for len(trace) > 0 { + n, err := h.writeTrace(trace) + if err != nil { + log.Error("Lost a trace: %s", err.cause) + h.config.statsd.Count("datadog.tracer.traces_dropped", 1, []string{"reason:" + err.dropReason}, 1) + return + } + trace = trace[n:] + // If there are traces left that didn't fit into the buffer, flush the buffer and loop to + // write the remaining spans. + if len(trace) > 0 { + h.flush() + } + } +} + +func (h *logTraceWriter) stop() { + h.config.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:shutdown"}, 1) + h.flush() +} + +// flush will write any buffered traces to standard output. +func (h *logTraceWriter) flush() { + if !h.hasTraces { + return + } + h.buf.WriteString(logBufferSuffix) + h.w.Write(h.buf.Bytes()) + h.resetBuffer() +} diff --git a/ddtrace/tracer/writer_test.go b/ddtrace/tracer/writer_test.go new file mode 100644 index 0000000000..f56f03c539 --- /dev/null +++ b/ddtrace/tracer/writer_test.go @@ -0,0 +1,298 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +func TestImplementsTraceWriter(t *testing.T) { + assert.Implements(t, (*traceWriter)(nil), &agentTraceWriter{}) + assert.Implements(t, (*traceWriter)(nil), &logTraceWriter{}) +} + +// makeSpan returns a span, adding n entries to meta and metrics each. +func makeSpan(n int) *span { + s := newSpan("encodeName", "encodeService", "encodeResource", random.Uint64(), random.Uint64(), random.Uint64()) + for i := 0; i < n; i++ { + istr := fmt.Sprintf("%0.10d", i) + s.Meta[istr] = istr + s.Metrics[istr] = float64(i) + } + return s +} + +func TestEncodeFloat(t *testing.T) { + for _, tt := range []struct { + f float64 + expect string + }{ + { + 9.9999999999999990e20, + "999999999999999900000", + }, + { + 9.9999999999999999e20, + "1e+21", + }, + { + -9.9999999999999990e20, + "-999999999999999900000", + }, + { + -9.9999999999999999e20, + "-1e+21", + }, + { + 0.000001, + "0.000001", + }, + { + 0.0000009, + "9e-7", + }, + { + -0.000001, + "-0.000001", + }, + { + -0.0000009, + "-9e-7", + }, + { + math.NaN(), + "null", + }, + { + math.Inf(-1), + "null", + }, + { + math.Inf(1), + "null", + }, + } { + t.Run(tt.expect, func(t *testing.T) { + assert.Equal(t, tt.expect, string(encodeFloat(nil, tt.f))) + }) + } + +} + +func TestLogWriter(t *testing.T) { + t.Run("basic", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + h := newLogTraceWriter(newConfig()) + h.w = &buf + s := makeSpan(0) + for i := 0; i < 20; i++ { + h.add([]*span{s, s}) + } + h.flush() + v := struct{ Traces [][]map[string]interface{} }{} + d := json.NewDecoder(&buf) + err := d.Decode(&v) + assert.NoError(err) + assert.Len(v.Traces, 20, "Expected 20 traces, but have %d", len(v.Traces)) + for _, t := range v.Traces { + assert.Len(t, 2, "Expected 2 spans, but have %d", len(t)) + } + err = d.Decode(&v) + assert.Equal(io.EOF, err) + }) + + t.Run("inf+nan", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + h := newLogTraceWriter(newConfig()) + h.w = &buf + s := makeSpan(0) + s.Metrics["nan"] = math.NaN() + s.Metrics["+inf"] = math.Inf(1) + s.Metrics["-inf"] = math.Inf(-1) + h.add([]*span{s}) + h.flush() + json := string(buf.Bytes()) + assert.NotContains(json, `"nan":`) + assert.NotContains(json, `"+inf":`) + assert.NotContains(json, `"-inf":`) + }) + + t.Run("fullspan", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + h := newLogTraceWriter(newConfig()) + h.w = &buf + type jsonSpan struct { + TraceID string `json:"trace_id"` + SpanID string `json:"span_id"` + ParentID string `json:"parent_id"` + Name string `json:"name"` + Resource string `json:"resource"` + Error int32 `json:"error"` + Meta map[string]string `json:"meta"` + Metrics map[string]float64 `json:"metrics"` + Start int64 `json:"start"` + Duration int64 `json:"duration"` + Service string `json:"service"` + } + type jsonPayload struct { + Traces [][]jsonSpan `json:"traces"` + } + s := &span{ + Name: "basicName", + Service: "basicService", + Resource: "basicResource", + Meta: map[string]string{ + "env": "prod", + "version": "1.26.0", + }, + Metrics: map[string]float64{ + "widgets": 1e26, + "zero": 0.0, + "big": math.MaxFloat64, + "small": math.SmallestNonzeroFloat64, + "nan": math.NaN(), + "-inf": math.Inf(-1), + "+inf": math.Inf(1), + }, + SpanID: 10, + TraceID: 11, + ParentID: 12, + Start: 123, + Duration: 456, + Error: 789, + } + expected := jsonSpan{ + Name: "basicName", + Service: "basicService", + Resource: "basicResource", + Meta: map[string]string{ + "env": "prod", + "version": "1.26.0", + }, + Metrics: map[string]float64{ + "widgets": 1e26, + "zero": 0.0, + "big": math.MaxFloat64, + "small": math.SmallestNonzeroFloat64, + }, + SpanID: "a", + TraceID: "b", + ParentID: "c", + Start: 123, + Duration: 456, + Error: 789, + } + h.add([]*span{s}) + h.flush() + d := json.NewDecoder(&buf) + var payload jsonPayload + err := d.Decode(&payload) + assert.NoError(err) + assert.Equal(jsonPayload{[][]jsonSpan{{expected}}}, payload) + }) +} + +func TestLogWriterOverflow(t *testing.T) { + log.UseLogger(new(testLogger)) + t.Run("single-too-big", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + var tg testStatsdClient + h := newLogTraceWriter(newConfig(withStatsdClient(&tg))) + h.w = &buf + s := makeSpan(10000) + h.add([]*span{s}) + h.flush() + v := struct{ Traces [][]map[string]interface{} }{} + d := json.NewDecoder(&buf) + err := d.Decode(&v) + assert.Equal(io.EOF, err) + assert.Contains(tg.CallNames(), "datadog.tracer.traces_dropped") + }) + + t.Run("split", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + var tg testStatsdClient + h := newLogTraceWriter(newConfig(withStatsdClient(&tg))) + h.w = &buf + s := makeSpan(10) + var trace []*span + for i := 0; i < 500; i++ { + trace = append(trace, s) + } + h.add(trace) + h.flush() + v := struct{ Traces [][]map[string]interface{} }{} + d := json.NewDecoder(&buf) + err := d.Decode(&v) + assert.NoError(err) + assert.Len(v.Traces, 1, "Expected 1 trace, but have %d", len(v.Traces)) + spann := len(v.Traces[0]) + err = d.Decode(&v) + assert.NoError(err) + assert.Len(v.Traces, 1, "Expected 1 trace, but have %d", len(v.Traces)) + spann += len(v.Traces[0]) + assert.Equal(500, spann) + err = d.Decode(&v) + assert.Equal(io.EOF, err) + }) + + t.Run("two-large", func(t *testing.T) { + assert := assert.New(t) + var buf bytes.Buffer + h := newLogTraceWriter(newConfig()) + h.w = &buf + s := makeSpan(4000) + h.add([]*span{s}) + h.add([]*span{s}) + h.flush() + v := struct{ Traces [][]map[string]interface{} }{} + d := json.NewDecoder(&buf) + err := d.Decode(&v) + assert.NoError(err) + assert.Len(v.Traces, 1, "Expected 1 trace, but have %d", len(v.Traces)) + assert.Len(v.Traces[0], 1, "Expected 1 span, but have %d", len(v.Traces[0])) + err = d.Decode(&v) + assert.NoError(err) + assert.Len(v.Traces, 1, "Expected 1 trace, but have %d", len(v.Traces)) + assert.Len(v.Traces[0], 1, "Expected 1 span, but have %d", len(v.Traces[0])) + err = d.Decode(&v) + assert.Equal(io.EOF, err) + }) +} + +func BenchmarkJsonEncodeSpan(b *testing.B) { + s := makeSpan(10) + s.Metrics["nan"] = math.NaN() + s.Metrics["+inf"] = math.Inf(1) + s.Metrics["-inf"] = math.Inf(-1) + h := &logTraceWriter{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + h.resetBuffer() + h.encodeSpan(s) + } +} + +func BenchmarkJsonEncodeFloat(b *testing.B) { + for i := 0; i < b.N; i++ { + var ba = make([]byte, 25) + bs := ba[:0] + encodeFloat(bs, float64(1e-9)) + } +} diff --git a/internal/log/log.go b/internal/log/log.go index a0bb8ef31b..e74f64b119 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -38,6 +38,7 @@ var ( // UseLogger sets l as the active logger. func UseLogger(l ddtrace.Logger) { + Flush() mu.Lock() defer mu.Unlock() logger = l From c67216eca605b69abb108299e31012037a49ca2d Mon Sep 17 00:00:00 2001 From: Justin DiPierro Date: Wed, 16 Sep 2020 16:14:20 -0400 Subject: [PATCH 10/22] contrib/go-redis/redis: Enable tracing of Pipelined and TxPipelined (#724) Fixes #720 --- contrib/go-redis/redis/redis.go | 24 +++++++++++++++ contrib/go-redis/redis/redis_test.go | 44 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/contrib/go-redis/redis/redis.go b/contrib/go-redis/redis/redis.go index 71b7c883e6..f504878f10 100644 --- a/contrib/go-redis/redis/redis.go +++ b/contrib/go-redis/redis/redis.go @@ -86,6 +86,21 @@ func (c *Client) Pipeline() redis.Pipeliner { return &Pipeliner{c.Client.Pipeline(), c.params, c.Client.Context()} } +// Pipelined executes a function parameter to build a Pipeline and then immediately executes it. +func (c *Client) Pipelined(fn func(redis.Pipeliner) error) ([]redis.Cmder, error) { + return c.Pipeline().Pipelined(fn) +} + +// TxPipelined executes a function parameter to build a Transactional Pipeline and then immediately executes it. +func (c *Client) TxPipelined(fn func(redis.Pipeliner) error) ([]redis.Cmder, error) { + return c.TxPipeline().Pipelined(fn) +} + +// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC. +func (c *Client) TxPipeline() redis.Pipeliner { + return &Pipeliner{c.Client.TxPipeline(), c.params, c.Client.Context()} +} + // 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 *Pipeliner) ExecWithContext(ctx context.Context) ([]redis.Cmder, error) { @@ -133,6 +148,15 @@ func commandsToString(cmds []redis.Cmder) string { return b.String() } +// Pipelined executes a function parameter to build a Pipeline and then immediately executes the built pipeline. +func (c *Pipeliner) Pipelined(fn func(redis.Pipeliner) error) ([]redis.Cmder, error) { + if err := fn(c); err != nil { + return nil, err + } + defer c.Close() + return c.Exec() +} + // WithContext sets a context on a Client. Use it to ensure that emitted spans have the correct parent. func (c *Client) WithContext(ctx context.Context) *Client { clone := &Client{ diff --git a/contrib/go-redis/redis/redis_test.go b/contrib/go-redis/redis/redis_test.go index 05d218d855..4f0e55f5bb 100644 --- a/contrib/go-redis/redis/redis_test.go +++ b/contrib/go-redis/redis/redis_test.go @@ -144,6 +144,50 @@ func TestPipeline(t *testing.T) { assert.Equal("2", span.Tag("redis.pipeline_length")) } +func TestPipelined(t *testing.T) { + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + _, err := client.Pipelined(func(p redis.Pipeliner) error { + p.Expire("pipeline_counter", time.Hour) + return nil + }) + assert.NoError(err) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\n", span.Tag(ext.ResourceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("1", span.Tag("redis.pipeline_length")) + + mt.Reset() + _, err = client.Pipelined(func(p redis.Pipeliner) error { + p.Expire("pipeline_counter", time.Hour) + p.Expire("pipeline_counter_1", time.Minute) + return nil + }) + assert.NoError(err) + + spans = mt.FinishedSpans() + assert.Len(spans, 1) + + span = spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\nexpire pipeline_counter_1 60: false\n", span.Tag(ext.ResourceName)) + assert.Equal("2", span.Tag("redis.pipeline_length")) +} + func TestChildSpan(t *testing.T) { opts := &redis.Options{Addr: "127.0.0.1:6379"} assert := assert.New(t) From e7d03f2c9f20f6b31de1ae1635192e4f3109702e Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bronisz Date: Wed, 16 Sep 2020 22:36:58 +0200 Subject: [PATCH 11/22] contrib/garyburd/redigo: support go 1.15 (#731) go 1.15 doesn't support to create context from nil parent. This commit fixes a bug in the redigo integration that passesa nil context if one is not provided by the user. Fixes #732 --- contrib/garyburd/redigo/redigo.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contrib/garyburd/redigo/redigo.go b/contrib/garyburd/redigo/redigo.go index 9316430958..521c64fa11 100644 --- a/contrib/garyburd/redigo/redigo.go +++ b/contrib/garyburd/redigo/redigo.go @@ -116,13 +116,11 @@ func (tc Conn) newChildSpan(ctx context.Context) ddtrace.Span { // When passed a context.Context as the final argument, Do will ensure that any span created // inherits from this context. The rest of the arguments are passed through to the Redis server unchanged. func (tc Conn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - var ( - ctx context.Context - ok bool - ) + ctx := context.Background() + if n := len(args); n > 0 { - ctx, ok = args[n-1].(context.Context) - if ok { + if argCtx, ok := args[n-1].(context.Context); ok { + ctx = argCtx args = args[:n-1] } } From ecb0b805ef25b00888a2fb62d465a5aa95e7301e Mon Sep 17 00:00:00 2001 From: Marcio Rodrigues Date: Thu, 17 Sep 2020 12:20:53 -0300 Subject: [PATCH 12/22] contrib/net/http: Add error to span in roundTripper (#736) This commit causes the contrib/net/http.roundTripper to set an error on the span when it receives a 500-range http response, or when there is an error sending the request or receiving the response. This makes the roundtripper consistent with other integrations such as contrib/gin-gonic/gin (see #449) Fixes #728 --- contrib/net/http/roundtripper.go | 2 + contrib/net/http/roundtripper_test.go | 96 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/contrib/net/http/roundtripper.go b/contrib/net/http/roundtripper.go index 96a329760d..50bdba74d5 100644 --- a/contrib/net/http/roundtripper.go +++ b/contrib/net/http/roundtripper.go @@ -55,11 +55,13 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er res, err = rt.base.RoundTrip(req.WithContext(ctx)) if err != nil { span.SetTag("http.errors", err.Error()) + span.SetTag(ext.Error, err) } else { span.SetTag(ext.HTTPCode, strconv.Itoa(res.StatusCode)) // treat 5XX as errors if res.StatusCode/100 == 5 { span.SetTag("http.errors", res.Status) + span.SetTag(ext.Error, fmt.Errorf("%d: %s", res.StatusCode, http.StatusText(res.StatusCode))) } } return res, err diff --git a/contrib/net/http/roundtripper_test.go b/contrib/net/http/roundtripper_test.go index eb762fc33a..035a5d54b7 100644 --- a/contrib/net/http/roundtripper_test.go +++ b/contrib/net/http/roundtripper_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" @@ -67,6 +68,101 @@ func TestRoundTripper(t *testing.T) { assert.Equal(t, true, s1.Tag("CalledAfter")) } +func TestRoundTripperServerError(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)) + assert.NoError(t, err) + + span := tracer.StartSpan("test", + tracer.ChildOf(spanctx)) + defer span.Finish() + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error")) + })) + defer s.Close() + + rt := WrapRoundTripper(http.DefaultTransport, + WithBefore(func(req *http.Request, span ddtrace.Span) { + span.SetTag("CalledBefore", true) + }), + WithAfter(func(res *http.Response, span ddtrace.Span) { + span.SetTag("CalledAfter", true) + })) + + client := &http.Client{ + Transport: rt, + } + + client.Get(s.URL + "/hello/world") + + spans := mt.FinishedSpans() + assert.Len(t, spans, 2) + assert.Equal(t, spans[0].TraceID(), spans[1].TraceID()) + + s0 := spans[0] + assert.Equal(t, "test", s0.OperationName()) + assert.Equal(t, "test", s0.Tag(ext.ResourceName)) + + s1 := spans[1] + assert.Equal(t, "http.request", s1.OperationName()) + assert.Equal(t, "http.request", s1.Tag(ext.ResourceName)) + assert.Equal(t, "500", s1.Tag(ext.HTTPCode)) + assert.Equal(t, "GET", s1.Tag(ext.HTTPMethod)) + assert.Equal(t, "/hello/world", s1.Tag(ext.HTTPURL)) + assert.Equal(t, fmt.Errorf("500: Internal Server Error"), s1.Tag(ext.Error)) + assert.Equal(t, true, s1.Tag("CalledBefore")) + assert.Equal(t, true, s1.Tag("CalledAfter")) +} + +func TestRoundTripperNetworkError(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)) + assert.NoError(t, err) + + span := tracer.StartSpan("test", + tracer.ChildOf(spanctx)) + defer span.Finish() + time.Sleep(10 * time.Millisecond) + w.Write([]byte("Timeout")) + })) + defer s.Close() + + rt := WrapRoundTripper(http.DefaultTransport, + WithBefore(func(req *http.Request, span ddtrace.Span) { + span.SetTag("CalledBefore", true) + }), + WithAfter(func(res *http.Response, span ddtrace.Span) { + span.SetTag("CalledAfter", true) + })) + + client := &http.Client{ + Transport: rt, + Timeout: 1 * time.Millisecond, + } + + client.Get(s.URL + "/hello/world") + + spans := mt.FinishedSpans() + assert.Len(t, spans, 1) + + s0 := spans[0] + assert.Equal(t, "http.request", s0.OperationName()) + assert.Equal(t, "http.request", s0.Tag(ext.ResourceName)) + assert.Equal(t, nil, s0.Tag(ext.HTTPCode)) + assert.Equal(t, "GET", s0.Tag(ext.HTTPMethod)) + assert.Equal(t, "/hello/world", s0.Tag(ext.HTTPURL)) + assert.NotNil(t, s0.Tag(ext.Error)) + assert.Equal(t, true, s0.Tag("CalledBefore")) + assert.Equal(t, true, s0.Tag("CalledAfter")) +} + func TestWrapClient(t *testing.T) { c := WrapClient(http.DefaultClient) assert.Equal(t, c, http.DefaultClient) From fc56ee060d048ef77b8e380575d30efbfe185f8b Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Tue, 22 Sep 2020 08:12:30 -0500 Subject: [PATCH 13/22] ddtrace/tracer: add WithDebugStack StartOption to configure stack traces. (#739) By default a stack trace is collected when a span is finished with an error. This can be disabled by passing tracer.NoDebugStack to span.Finish(). Sometimes it is desirable to disable this option globally, which is now possible with the WithDebugStack StartOption that this commit introduces. --- ddtrace/tracer/option.go | 13 +++++++++++++ ddtrace/tracer/span.go | 11 +++++++---- ddtrace/tracer/tracer.go | 15 ++++++++------- ddtrace/tracer/tracer_test.go | 16 +++++++++++++++- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index d5c52259ba..d49a9a5065 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -92,6 +92,10 @@ type config struct { // tickChan specifies a channel which will receive the time every time the tracer must flush. // It defaults to time.Ticker; replaced in tests. tickChan <-chan time.Time + + // noDebugStack disables the collection of debug stack traces globally. No traces reporting + // errors will record a stack trace when this option is set. + noDebugStack bool } // StartOption represents a function that can be provided as a parameter to Start. @@ -249,6 +253,15 @@ func WithPrioritySampling() StartOption { } } +// WithDebugStack can be used to globally enable or disable the collection of stack traces when +// spans finish with errors. It is enabled by default. This is a global version of the NoDebugStack +// FinishOption. +func WithDebugStack(enabled bool) StartOption { + return func(c *config) { + c.noDebugStack = !enabled + } +} + // WithDebugMode enables debug mode on the tracer, resulting in more verbose logging. func WithDebugMode(enabled bool) StartOption { return func(c *config) { diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 6eab989601..ae690390ad 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -67,9 +67,10 @@ type span struct { ParentID uint64 `msg:"parent_id"` // identifier of the span's direct parent Error int32 `msg:"error"` // error status of the span; 0 means no errors - finished bool `msg:"-"` // true if the span has been submitted to a tracer. - context *spanContext `msg:"-"` // span propagation context - taskEnd func() // ends execution tracer (runtime/trace) task, if started + noDebugStack bool `msg:"-"` // disables debug stack traces + finished bool `msg:"-"` // true if the span has been submitted to a tracer. + context *spanContext `msg:"-"` // span propagation context + taskEnd func() // ends execution tracer (runtime/trace) task, if started } // Context yields the SpanContext for this Span. Note that the return @@ -260,7 +261,9 @@ func (s *span) setMetric(key string, v float64) { func (s *span) Finish(opts ...ddtrace.FinishOption) { t := now() if len(opts) > 0 { - var cfg ddtrace.FinishConfig + cfg := ddtrace.FinishConfig{ + NoDebugStack: s.noDebugStack, + } for _, fn := range opts { fn(&cfg) } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 973ed4b0c7..e61d132bfc 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -257,13 +257,14 @@ func (t *tracer) StartSpan(operationName string, options ...ddtrace.StartSpanOpt } // span defaults span := &span{ - Name: operationName, - Service: t.config.serviceName, - Resource: operationName, - SpanID: id, - TraceID: id, - Start: startTime, - taskEnd: startExecutionTracerTask(operationName), + Name: operationName, + Service: t.config.serviceName, + Resource: operationName, + SpanID: id, + TraceID: id, + Start: startTime, + taskEnd: startExecutionTracerTask(operationName), + noDebugStack: t.config.noDebugStack, } if context != nil { // this is a child span diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index ff4ee2c92b..25d079b059 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -7,6 +7,7 @@ package tracer import ( "context" + "errors" "io" "io/ioutil" "net/http" @@ -59,7 +60,7 @@ loop: } } -// TestTracerFrenetic does frenetic testing in a scenario where the tracer is started +// TestTracerCleanStop does frenetic testing in a scenario where the tracer is started // and stopped in parallel with spans being created. func TestTracerCleanStop(t *testing.T) { if testing.Short() { @@ -442,6 +443,19 @@ func TestTracerSpanGlobalTags(t *testing.T) { assert.Equal("value", child.Meta["key"]) } +func TestTracerNoDebugStack(t *testing.T) { + assert := assert.New(t) + tracer := newTracer(WithDebugStack(false)) + s := tracer.StartSpan("web.request").(*span) + err := errors.New("test error") + s.Finish(WithError(err)) + + assert.Equal(int32(1), s.Error) + assert.Equal("test error", s.Meta[ext.ErrorMsg]) + assert.Equal("*errors.errorString", s.Meta[ext.ErrorType]) + assert.Empty(s.Meta[ext.ErrorStack]) +} + func TestNewSpan(t *testing.T) { assert := assert.New(t) From 446165ca5a503c1245af4c6f591084ec93f6f4e6 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Tue, 22 Sep 2020 09:13:28 -0500 Subject: [PATCH 14/22] ddtrace/tracer: disable agent connectivity check in lambda mode (#742) When the tracer is running in a lambda environment, we should disable the agent connectivity check on startup to avoid misleading error messages. --- ddtrace/tracer/log.go | 10 +++++++--- ddtrace/tracer/log_test.go | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ddtrace/tracer/log.go b/ddtrace/tracer/log.go index 709c4a3ed1..0ac93d607a 100644 --- a/ddtrace/tracer/log.go +++ b/ddtrace/tracer/log.go @@ -46,6 +46,7 @@ type startupInfo struct { ApplicationVersion string `json:"dd_version"` // Version of the user's application Architecture string `json:"architecture"` // Architecture of host machine GlobalService string `json:"global_service"` // Global service string. If not-nil should be same as Service. (#614) + LambdaMode string `json:"lambda_mode"` // Whether or not the client has enabled lambda mode } // checkEndpoint tries to connect to the URL specified by endpoint. @@ -93,13 +94,16 @@ func logStartup(t *tracer) { ApplicationVersion: t.config.version, Architecture: runtime.GOARCH, GlobalService: globalconfig.ServiceName(), + LambdaMode: fmt.Sprintf("%t", t.config.logToStdout), } if _, err := samplingRulesFromEnv(); err != nil { info.SamplingRulesError = fmt.Sprintf("%s", err) } - if err := checkEndpoint(t.transport.endpoint()); err != nil { - info.AgentError = fmt.Sprintf("%s", err) - log.Warn("DIAGNOSTICS Unable to reach agent: %s", err) + if !t.config.logToStdout { + if err := checkEndpoint(t.transport.endpoint()); err != nil { + info.AgentError = fmt.Sprintf("%s", err) + log.Warn("DIAGNOSTICS Unable to reach agent: %s", err) + } } bs, err := json.Marshal(info) if err != nil { diff --git a/ddtrace/tracer/log_test.go b/ddtrace/tracer/log_test.go index 61d03c9873..79c5a83717 100644 --- a/ddtrace/tracer/log_test.go +++ b/ddtrace/tracer/log_test.go @@ -25,7 +25,7 @@ func TestStartupLog(t *testing.T) { tp.Reset() logStartup(tracer) assert.Len(tp.Lines(), 2) - assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":null,"sampling_rules_error":"","tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":""}`, tp.Lines()[1]) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":null,"sampling_rules_error":"","tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false"}`, tp.Lines()[1]) }) t.Run("configured", func(t *testing.T) { @@ -53,7 +53,7 @@ func TestStartupLog(t *testing.T) { tp.Reset() logStartup(tracer) assert.Len(tp.Lines(), 2) - assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sampling_rules":\[{"service":"mysql","name":"","sample_rate":0\.75}\],"sampling_rules_error":"","tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"health_metrics_enabled":true,"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service"}`, tp.Lines()[1]) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sampling_rules":\[{"service":"mysql","name":"","sample_rate":0\.75}\],"sampling_rules_error":"","tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"health_metrics_enabled":true,"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false"}`, tp.Lines()[1]) }) t.Run("errors", func(t *testing.T) { @@ -67,7 +67,19 @@ func TestStartupLog(t *testing.T) { tp.Reset() logStartup(tracer) assert.Len(tp.Lines(), 2) - assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":\[{"service":"some.service","name":"","sample_rate":0\.234}\],"sampling_rules_error":"found errors:\\n\\tat index 1: rate not provided","tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":""}`, tp.Lines()[1]) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":\[{"service":"some.service","name":"","sample_rate":0\.234}\],"sampling_rules_error":"found errors:\\n\\tat index 1: rate not provided","tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false"}`, tp.Lines()[1]) + }) + + t.Run("lambda", func(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + tracer, _, _, stop := startTestTracer(t, WithLogger(tp), WithLambdaMode(true)) + defer stop() + + tp.Reset() + logStartup(tracer) + assert.Len(tp.Lines(), 1) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":null,"sampling_rules_error":"","tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"true"}`, tp.Lines()[0]) }) } From ec564257e1787bb2470c30137a004d569d273f3d Mon Sep 17 00:00:00 2001 From: Mickey Reiss Date: Tue, 22 Sep 2020 08:43:24 -0700 Subject: [PATCH 15/22] contrib/twitchtv/twirp: Add explicit reference to the request span (#726) This commit refactors the twirp serverhooks so that the specific span that was started in requestRoutedHook is finished in responseSentHook. This way, other server hooks may start new spans in the context without conflicting with this middleware. Fixes #725. Co-authored-by: Kyle Nusbaum --- contrib/twitchtv/twirp/twirp.go | 29 +++++++++++++++------- contrib/twitchtv/twirp/twirp_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/contrib/twitchtv/twirp/twirp.go b/contrib/twitchtv/twirp/twirp.go index dae161b840..e4382836c0 100644 --- a/contrib/twitchtv/twirp/twirp.go +++ b/contrib/twitchtv/twirp/twirp.go @@ -21,10 +21,9 @@ import ( "github.com/twitchtv/twirp" ) -type contextKey int - -const ( - twirpErrorKey contextKey = iota +type ( + twirpErrorKey struct{} + twirpSpanKey struct{} ) // HTTPClient is duplicated from twirp's generated service code. @@ -167,15 +166,23 @@ func requestReceivedHook(cfg *config) func(context.Context) (context.Context, er if !math.IsNaN(cfg.analyticsRate) { opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) } - _, ctx = tracer.StartSpanFromContext(ctx, spanNameFromContext(ctx), opts...) + span, ctx := tracer.StartSpanFromContext(ctx, spanNameFromContext(ctx), opts...) + + ctx = context.WithValue(ctx, twirpSpanKey{}, span) return ctx, nil } } func requestRoutedHook(cfg *config) func(context.Context) (context.Context, error) { return func(ctx context.Context) (context.Context, error) { - span, ok := tracer.SpanFromContext(ctx) + maybeSpan := ctx.Value(twirpSpanKey{}) + if maybeSpan == nil { + log.Error("contrib/twitchtv/twirp.requestRoutedHook: found no span in context") + return ctx, nil + } + span, ok := maybeSpan.(tracer.Span) if !ok { + log.Error("contrib/twitchtv/twirp.requestRoutedHook: found invalid span type in context") return ctx, nil } if method, ok := twirp.MethodName(ctx); ok { @@ -194,20 +201,24 @@ func responsePreparedHook(cfg *config) func(context.Context) context.Context { func responseSentHook(cfg *config) func(context.Context) { return func(ctx context.Context) { - span, ok := tracer.SpanFromContext(ctx) + maybeSpan := ctx.Value(twirpSpanKey{}) + if maybeSpan == nil { + return + } + span, ok := maybeSpan.(tracer.Span) if !ok { return } if sc, ok := twirp.StatusCode(ctx); ok { span.SetTag(ext.HTTPCode, sc) } - err, _ := ctx.Value(twirpErrorKey).(twirp.Error) + err, _ := ctx.Value(twirpErrorKey{}).(twirp.Error) span.Finish(tracer.WithError(err)) } } func errorHook(cfg *config) func(context.Context, twirp.Error) context.Context { return func(ctx context.Context, err twirp.Error) context.Context { - return context.WithValue(ctx, twirpErrorKey, err) + return context.WithValue(ctx, twirpErrorKey{}, err) } } diff --git a/contrib/twitchtv/twirp/twirp_test.go b/contrib/twitchtv/twirp/twirp_test.go index 02a2e702d5..6994f14e99 100644 --- a/contrib/twitchtv/twirp/twirp_test.go +++ b/contrib/twitchtv/twirp/twirp_test.go @@ -14,6 +14,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "github.com/stretchr/testify/assert" @@ -198,6 +199,41 @@ func TestServerHooks(t *testing.T) { assert.Equal("500", span.Tag(ext.HTTPCode)) assert.Equal("twirp error internal: something bad or unexpected happened", span.Tag(ext.Error).(error).Error()) }) + + t.Run("chained", func(t *testing.T) { + defer mt.Reset() + assert := assert.New(t) + + otherHooks := &twirp.ServerHooks{ + RequestReceived: func(ctx context.Context) (context.Context, error) { + _, ctx = tracer.StartSpanFromContext(ctx, "other.span.name") + return ctx, nil + }, + ResponseSent: func(ctx context.Context) { + span, ok := tracer.SpanFromContext(ctx) + if !ok { + return + } + span.Finish() + }, + } + mockServer(twirp.ChainHooks(hooks, otherHooks), assert, twirp.InternalError("something bad or unexpected happened")) + + spans := mt.FinishedSpans() + assert.Len(spans, 2) + span := spans[0] + assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) + assert.Equal("twirp-test", span.Tag(ext.ServiceName)) + assert.Equal("twirp.Example", span.OperationName()) + assert.Equal("twirp.test", span.Tag("twirp.package")) + assert.Equal("Example", span.Tag("twirp.service")) + assert.Equal("Method", span.Tag("twirp.method")) + assert.Equal("500", span.Tag(ext.HTTPCode)) + assert.Equal("twirp error internal: something bad or unexpected happened", span.Tag(ext.Error).(error).Error()) + + span = spans[1] + assert.Equal("other.span.name", span.OperationName()) + }) } func TestAnalyticsSettings(t *testing.T) { From 67df74cccdc699fba9a5971e46a88946f049e3c0 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 1 Oct 2020 18:27:16 -0500 Subject: [PATCH 16/22] contrib: add note to README.md to clarify package naming under version suffixes (#746) --- contrib/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/README.md b/contrib/README.md index b31c502aae..a802c027c4 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -17,6 +17,7 @@ First, find the library which you'd like to integrate with. The naming conventio * If the package is hosted on Github (eg. `github.com/user/repo`) and has version `v2.1.0`, it will be located at the shorthand path `user/repo.v2`. * If the package is from anywhere else (eg. `google.golang.org/grpc`) and has no stable version, it can be found under the full import path, followed by the version suffix (in this example `.v0`). * All new integrations should be suffixed with `.vN` where `N` is the major version that is being covered. +* The package itself should retain its un-versioned name. For example, the integration under `user/repo.v2` stays as `package repo`, and does not become `package repo.v2` Each integration comes with thorough documentation and usage examples. A good overview can be seen on our [godoc](https://godoc.org/gopkg.in/DataDog/dd-trace-go.v1/contrib) page. From 67c15f788728fce349446d010c1e7aabf497f9a4 Mon Sep 17 00:00:00 2001 From: adw1n Date: Fri, 2 Oct 2020 01:31:11 +0200 Subject: [PATCH 17/22] contrib/go-chi/chi: Handle 0 status code correctly (#740) When the user doesn't call w.WriteHeader(...) or w.Write(...) in the request handler or any of the middlewares the status isn't set and WrapResponseWriter.Status() returns 0. In such a case net/http library takes the matters into its own hands and writes 200 status code. This commit makes our middleware detect a zero status and record http.StatusOK in the request span. Previously, spans would show up with an empty status. --- contrib/go-chi/chi/chi.go | 4 ++ contrib/go-chi/chi/chi_test.go | 85 +++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/contrib/go-chi/chi/chi.go b/contrib/go-chi/chi/chi.go index f09329c8e5..3f2f29c90d 100644 --- a/contrib/go-chi/chi/chi.go +++ b/contrib/go-chi/chi/chi.go @@ -61,6 +61,10 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler { // set the status code status := ww.Status() + // 0 status means one has not yet been sent in which case net/http library will write StatusOK + if ww.Status() == 0 { + status = http.StatusOK + } span.SetTag(ext.HTTPCode, strconv.Itoa(status)) if status >= 500 && status < 600 { diff --git a/contrib/go-chi/chi/chi_test.go b/contrib/go-chi/chi/chi_test.go index 42afdae987..27a1328585 100644 --- a/contrib/go-chi/chi/chi_test.go +++ b/contrib/go-chi/chi/chi_test.go @@ -39,42 +39,63 @@ func TestChildSpan(t *testing.T) { } func TestTrace200(t *testing.T) { - assert := assert.New(t) - mt := mocktracer.Start() - defer mt.Stop() + assertDoRequest := func(assert *assert.Assertions, mt mocktracer.Tracer, router *chi.Mux) { + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() - router := chi.NewRouter() - router.Use(Middleware(WithServiceName("foobar"))) - router.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) { - span, ok := tracer.SpanFromContext(r.Context()) - assert.True(ok) - assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "foobar") - id := chi.URLParam(r, "id") - w.Write([]byte(id)) - }) + // do and verify the request + router.ServeHTTP(w, r) + response := w.Result() + assert.Equal(response.StatusCode, 200) - r := httptest.NewRequest("GET", "/user/123", nil) - w := httptest.NewRecorder() + // verify traces look good + spans := mt.FinishedSpans() + assert.Len(spans, 1) + if len(spans) < 1 { + t.Fatalf("no spans") + } + span := spans[0] + assert.Equal("http.request", span.OperationName()) + assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) + assert.Equal("foobar", span.Tag(ext.ServiceName)) + assert.Equal("GET /user/{id}", span.Tag(ext.ResourceName)) + assert.Equal("200", span.Tag(ext.HTTPCode)) + assert.Equal("GET", span.Tag(ext.HTTPMethod)) + assert.Equal("/user/123", span.Tag(ext.HTTPURL)) + } - // do and verify the request - router.ServeHTTP(w, r) - response := w.Result() - assert.Equal(response.StatusCode, 200) + t.Run("response written", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() - // verify traces look good - spans := mt.FinishedSpans() - assert.Len(spans, 1) - if len(spans) < 1 { - t.Fatalf("no spans") - } - span := spans[0] - assert.Equal("http.request", span.OperationName()) - assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) - assert.Equal("foobar", span.Tag(ext.ServiceName)) - assert.Equal("GET /user/{id}", span.Tag(ext.ResourceName)) - assert.Equal("200", span.Tag(ext.HTTPCode)) - assert.Equal("GET", span.Tag(ext.HTTPMethod)) - assert.Equal("/user/123", span.Tag(ext.HTTPURL)) + router := chi.NewRouter() + router.Use(Middleware(WithServiceName("foobar"))) + router.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) { + span, ok := tracer.SpanFromContext(r.Context()) + assert.True(ok) + assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "foobar") + id := chi.URLParam(r, "id") + _, err := w.Write([]byte(id)) + assert.NoError(err) + }) + assertDoRequest(assert, mt, router) + }) + + t.Run("no response written", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := chi.NewRouter() + router.Use(Middleware(WithServiceName("foobar"))) + router.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) { + span, ok := tracer.SpanFromContext(r.Context()) + assert.True(ok) + assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "foobar") + }) + assertDoRequest(assert, mt, router) + }) } func TestError(t *testing.T) { From c76a78775996998e13b92c870c82e6ed74edbb7e Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Fri, 2 Oct 2020 13:24:22 -0500 Subject: [PATCH 18/22] internal/version: bump version to 1.28.0 (#751) --- internal/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/version/version.go b/internal/version/version.go index 9099ddfa79..4a5fd2b909 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -8,4 +8,4 @@ package version // Tag specifies the current release tag. It needs to be manually // updated. A test checks that the value of Tag never points to a // git tag that is older than HEAD. -const Tag = "v1.27.0" +const Tag = "v1.28.0" From fb0d02c51140f3401a8f4c6ddaea985ddab8bf84 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 5 Oct 2020 17:43:04 +0200 Subject: [PATCH 19/22] Use existing const in osinfo_default (#754) Closes #753 --- ddtrace/tracer/osinfo_default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/tracer/osinfo_default.go b/ddtrace/tracer/osinfo_default.go index a5b8d3f14f..5f130a306b 100644 --- a/ddtrace/tracer/osinfo_default.go +++ b/ddtrace/tracer/osinfo_default.go @@ -16,5 +16,5 @@ func osName() string { } func osVersion() string { - return unknownVersion + return unknown } From 54b73b3e126a7489ed774c98aadd301488e44123 Mon Sep 17 00:00:00 2001 From: Sacha Froment Date: Mon, 5 Oct 2020 17:49:17 +0200 Subject: [PATCH 20/22] contrib/go-redis/redis.v8: support go-redis.v8 (#727) --- contrib/go-redis/redis.v8/example_test.go | 58 ++++ contrib/go-redis/redis.v8/option.go | 60 ++++ contrib/go-redis/redis.v8/redis.go | 142 +++++++++ contrib/go-redis/redis.v8/redis_test.go | 346 ++++++++++++++++++++++ 4 files changed, 606 insertions(+) create mode 100644 contrib/go-redis/redis.v8/example_test.go create mode 100644 contrib/go-redis/redis.v8/option.go create mode 100644 contrib/go-redis/redis.v8/redis.go create mode 100644 contrib/go-redis/redis.v8/redis_test.go diff --git a/contrib/go-redis/redis.v8/example_test.go b/contrib/go-redis/redis.v8/example_test.go new file mode 100644 index 0000000000..80e60b07a6 --- /dev/null +++ b/contrib/go-redis/redis.v8/example_test.go @@ -0,0 +1,58 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis_test + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis.v8" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// To start tracing Redis, simply create a new client using the library and continue +// using as you normally would. +func Example() { + ctx := context.Background() + // create a new Client + opts := &redis.Options{Addr: "127.0.0.1", Password: "", DB: 0} + c := redistrace.NewClient(opts) + + // any action emits a span + c.Set(ctx, "test_key", "test_value", 0) + + // optionally, create a new root span + root, ctx := tracer.StartSpanFromContext(context.Background(), "parent.request", + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName("web"), + tracer.ResourceName("/home"), + ) + + // commit further commands, which will inherit from the parent in the context. + c.Set(ctx, "food", "cheese", 0) + root.Finish() +} + +// You can also trace Redis Pipelines. Simply use as usual and the traces will be +// automatically picked up by the underlying implementation. +func Example_pipeliner() { + ctx := context.Background() + // create a client + opts := &redis.Options{Addr: "127.0.0.1", Password: "", DB: 0} + c := redistrace.NewClient(opts, redistrace.WithServiceName("my-redis-service")) + + // open the pipeline + pipe := c.Pipeline() + + // submit some commands + pipe.Incr(ctx, "pipeline_counter") + pipe.Expire(ctx, "pipeline_counter", time.Hour) + + // execute with trace + pipe.Exec(ctx) +} diff --git a/contrib/go-redis/redis.v8/option.go b/contrib/go-redis/redis.v8/option.go new file mode 100644 index 0000000000..dd8c660b19 --- /dev/null +++ b/contrib/go-redis/redis.v8/option.go @@ -0,0 +1,60 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis + +import ( + "math" + + "gopkg.in/DataDog/dd-trace-go.v1/internal" +) + +type clientConfig struct { + serviceName string + analyticsRate float64 +} + +// ClientOption represents an option that can be used to create or wrap a client. +type ClientOption func(*clientConfig) + +func defaults(cfg *clientConfig) { + cfg.serviceName = "redis.client" + // cfg.analyticsRate = globalconfig.AnalyticsRate() + if internal.BoolEnv("DD_TRACE_REDIS_ANALYTICS_ENABLED", false) { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } +} + +// WithServiceName sets the given service name for the client. +func WithServiceName(name string) ClientOption { + return func(cfg *clientConfig) { + cfg.serviceName = name + } +} + +// WithAnalytics enables Trace Analytics for all started spans. +func WithAnalytics(on bool) ClientOption { + return func(cfg *clientConfig) { + if on { + cfg.analyticsRate = 1.0 + } else { + cfg.analyticsRate = math.NaN() + } + } +} + +// WithAnalyticsRate sets the sampling rate for Trace Analytics events +// correlated to started spans. +func WithAnalyticsRate(rate float64) ClientOption { + return func(cfg *clientConfig) { + if rate >= 0.0 && rate <= 1.0 { + cfg.analyticsRate = rate + } else { + cfg.analyticsRate = math.NaN() + } + } +} diff --git a/contrib/go-redis/redis.v8/redis.go b/contrib/go-redis/redis.v8/redis.go new file mode 100644 index 0000000000..18722fdde3 --- /dev/null +++ b/contrib/go-redis/redis.v8/redis.go @@ -0,0 +1,142 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +// Package redis provides tracing functions for tracing the go-redis/redis package (https://github.com/go-redis/redis). +// This package supports versions up to go-redis 6.15. +package redis + +import ( + "bytes" + "context" + + "math" + "net" + "strconv" + "strings" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "github.com/go-redis/redis/v8" +) + +type datadogHook struct { + *params +} + +// params holds the tracer and a set of parameters which are recorded with every trace. +type params struct { + host string + port string + db string + config *clientConfig +} + +// NewClient returns a new Client that is traced with the default tracer under +// the service name "redis". +func NewClient(opt *redis.Options, opts ...ClientOption) redis.UniversalClient { + cfg := new(clientConfig) + defaults(cfg) + for _, fn := range opts { + fn(cfg) + } + host, port, err := net.SplitHostPort(opt.Addr) + if err != nil { + host = opt.Addr + port = "6379" + } + params := ¶ms{ + host: host, + port: port, + db: strconv.Itoa(opt.DB), + config: cfg, + } + client := redis.NewClient(opt) + client.AddHook(&datadogHook{params: params}) + return client +} + +func (ddh *datadogHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + raw := cmd.String() + parts := strings.Split(raw, " ") + length := len(parts) - 1 + p := ddh.params + opts := []ddtrace.StartSpanOption{ + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName(p.config.serviceName), + tracer.ResourceName(parts[0]), + tracer.Tag(ext.TargetHost, p.host), + tracer.Tag(ext.TargetPort, p.port), + tracer.Tag("out.db", p.db), + tracer.Tag("redis.raw_command", raw), + tracer.Tag("redis.args_length", strconv.Itoa(length)), + } + if !math.IsNaN(p.config.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate)) + } + _, ctx = tracer.StartSpanFromContext(ctx, "redis.command", opts...) + return ctx, nil +} + +func (ddh *datadogHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + var span tracer.Span + span, _ = tracer.SpanFromContext(ctx) + var finishOpts []ddtrace.FinishOption + errRedis := cmd.Err() + if errRedis != redis.Nil { + finishOpts = append(finishOpts, tracer.WithError(errRedis)) + } + span.Finish(finishOpts...) + return nil +} + +func (ddh *datadogHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + raw := commandsToString(cmds) + parts := strings.Split(raw, " ") + length := len(parts) - 1 + p := ddh.params + opts := []ddtrace.StartSpanOption{ + tracer.SpanType(ext.SpanTypeRedis), + tracer.ServiceName(p.config.serviceName), + tracer.ResourceName(parts[0]), + tracer.Tag(ext.TargetHost, p.host), + tracer.Tag(ext.TargetPort, p.port), + tracer.Tag("out.db", p.db), + tracer.Tag("redis.raw_command", raw), + tracer.Tag("redis.args_length", strconv.Itoa(length)), + tracer.Tag(ext.ResourceName, raw), + tracer.Tag("redis.pipeline_length", strconv.Itoa(len(cmds))), + } + if !math.IsNaN(p.config.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, p.config.analyticsRate)) + } + _, ctx = tracer.StartSpanFromContext(ctx, "redis.command", opts...) + return ctx, nil +} + +func (ddh *datadogHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + var span tracer.Span + span, _ = tracer.SpanFromContext(ctx) + var finishOpts []ddtrace.FinishOption + for _, cmd := range cmds { + errCmd := cmd.Err() + if errCmd != redis.Nil { + finishOpts = append(finishOpts, tracer.WithError(errCmd)) + } + } + span.Finish(finishOpts...) + return nil +} + +// commandsToString returns a string representation of a slice of redis Commands, separated by newlines. +func commandsToString(cmds []redis.Cmder) string { + var b bytes.Buffer + for _, cmd := range cmds { + b.WriteString(cmd.String()) + b.WriteString("\n") + } + return b.String() +} diff --git a/contrib/go-redis/redis.v8/redis_test.go b/contrib/go-redis/redis.v8/redis_test.go new file mode 100644 index 0000000000..b40b306cb5 --- /dev/null +++ b/contrib/go-redis/redis.v8/redis_test.go @@ -0,0 +1,346 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package redis + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +const debug = false + +// ensure it's a redis.Hook +var _ redis.Hook = (*datadogHook)(nil) + +func TestMain(m *testing.M) { + _, ok := os.LookupEnv("INTEGRATION") + if !ok { + fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable") + os.Exit(0) + } + os.Exit(m.Run()) +} + +func TestClientEvalSha(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + + sha1 := client.ScriptLoad(ctx, "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}").Val() + mt.Reset() + + client.EvalSha(ctx, sha1, []string{"key1", "key2", "first", "second"}) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("evalsha", span.Tag(ext.ResourceName)) +} + +func TestClient(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + client.Set(ctx, "test_key", "test_value", 0) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("set test_key test_value: ", span.Tag("redis.raw_command")) + assert.Equal("3", span.Tag("redis.args_length")) +} + +func TestPipeline(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + pipeline := client.Pipeline() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + + // Exec with context test + pipeline.Exec(ctx) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\n", span.Tag(ext.ResourceName)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("1", span.Tag("redis.pipeline_length")) + + mt.Reset() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + pipeline.Expire(ctx, "pipeline_counter_1", time.Minute) + + // Rewriting Exec + pipeline.Exec(ctx) + + spans = mt.FinishedSpans() + assert.Len(spans, 1) + + span = spans[0] + assert.Equal("redis.command", span.OperationName()) + assert.Equal(ext.SpanTypeRedis, span.Tag(ext.SpanType)) + assert.Equal("my-redis", span.Tag(ext.ServiceName)) + assert.Equal("expire pipeline_counter 3600: false\nexpire pipeline_counter_1 60: false\n", span.Tag(ext.ResourceName)) + assert.Equal("2", span.Tag("redis.pipeline_length")) +} + +func TestChildSpan(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + // Parent span + client := NewClient(opts, WithServiceName("my-redis")) + root, ctx := tracer.StartSpanFromContext(ctx, "parent.span") + + client.Set(ctx, "test_key", "test_value", 0) + root.Finish() + + spans := mt.FinishedSpans() + assert.Len(spans, 2) + + var child, parent mocktracer.Span + for _, s := range spans { + // order of traces in buffer is not garanteed + switch s.OperationName() { + case "redis.command": + child = s + case "parent.span": + parent = s + } + } + assert.NotNil(parent) + assert.NotNil(child) + + assert.Equal(child.ParentID(), parent.SpanID()) + assert.Equal(child.Tag(ext.TargetHost), "127.0.0.1") + assert.Equal(child.Tag(ext.TargetPort), "6379") +} + +func TestMultipleCommands(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + client.Set(ctx, "test_key", "test_value", 0) + client.Get(ctx, "test_key") + client.Incr(ctx, "int_key") + client.ClientList(ctx) + + spans := mt.FinishedSpans() + assert.Len(spans, 4) + + // Checking all commands were recorded + var commands [4]string + for i := 0; i < 4; i++ { + commands[i] = spans[i].Tag("redis.raw_command").(string) + } + 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) { + t.Run("wrong-port", func(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6378"} // wrong port + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + _, err := client.Get(ctx, "key").Result() + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + span := spans[0] + + assert.Equal("redis.command", span.OperationName()) + assert.NotNil(err) + assert.Equal(err, span.Tag(ext.Error)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6378", span.Tag(ext.TargetPort)) + assert.Equal("get key: ", span.Tag("redis.raw_command")) + }) + + t.Run("nil", func(t *testing.T) { + ctx := context.Background() + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + client := NewClient(opts, WithServiceName("my-redis")) + _, err := client.Get(ctx, "non_existent_key").Result() + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + span := spans[0] + + assert.Equal(redis.Nil, err) + assert.Equal("redis.command", span.OperationName()) + assert.Empty(span.Tag(ext.Error)) + assert.Equal("127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal("6379", span.Tag(ext.TargetPort)) + assert.Equal("get non_existent_key: ", span.Tag("redis.raw_command")) + }) +} +func TestAnalyticsSettings(t *testing.T) { + assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...ClientOption) { + ctx := context.Background() + client := NewClient(&redis.Options{Addr: "127.0.0.1:6379"}, opts...) + client.Set(ctx, "test_key", "test_value", 0) + pipeline := client.Pipeline() + pipeline.Expire(ctx, "pipeline_counter", time.Hour) + pipeline.Exec(ctx) + + spans := mt.FinishedSpans() + assert.Len(t, spans, 2) + for _, s := range spans { + assert.Equal(t, rate, s.Tag(ext.EventSampleRate)) + } + } + + t.Run("defaults", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil) + }) + + t.Run("global", func(t *testing.T) { + t.Skip("global flag disabled") + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.4) + }) + + t.Run("enabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, 1.0, WithAnalytics(true)) + }) + + t.Run("disabled", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, nil, WithAnalytics(false)) + }) + + t.Run("override", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + rate := globalconfig.AnalyticsRate() + defer globalconfig.SetAnalyticsRate(rate) + globalconfig.SetAnalyticsRate(0.4) + + assertRate(t, mt, 0.23, WithAnalyticsRate(0.23)) + }) + + t.Run("zero", func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + assertRate(t, mt, 0.0, WithAnalyticsRate(0.0)) + }) +} + +func TestWithContext(t *testing.T) { + opts := &redis.Options{Addr: "127.0.0.1:6379"} + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + ctx1 := context.Background() + ctx2 := context.Background() + client1 := NewClient(opts, WithServiceName("my-redis")) + s1, ctx1 := tracer.StartSpanFromContext(ctx1, "span1.name") + + s2, ctx2 := tracer.StartSpanFromContext(ctx2, "span2.name") + client2 := NewClient(opts, WithServiceName("my-redis")) + client1.Set(ctx1, "test_key", "test_value", 0) + client2.Get(ctx2, "test_key") + s1.Finish() + s2.Finish() + + spans := mt.FinishedSpans() + assert.Len(spans, 4) + var span1, span2, setSpan, getSpan mocktracer.Span + for _, s := range spans { + switch s.Tag(ext.ResourceName) { + case "span1.name": + span1 = s + case "span2.name": + span2 = s + case "set": + setSpan = s + case "get": + getSpan = s + } + } + + assert.NotNil(span1) + assert.NotNil(span2) + assert.NotNil(setSpan) + assert.NotNil(getSpan) + assert.Equal(span1.SpanID(), setSpan.ParentID()) + assert.Equal(span2.SpanID(), getSpan.ParentID()) +} From 6514d0bd5a2112f81b24d1520111ccac339f1e9f Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Thu, 15 Oct 2020 06:56:32 -0500 Subject: [PATCH 21/22] contrib/Shopify/sarama: use sarama.MinVersion in tests. (#761) Starting in github.com/Shopify/sarama@v1.27.1, the default kafka version that the client uses is 1.0.0 (sarama.V1_0_0_0) rather that the previous 0.8.2 (sarama.V0_8_2_0) Unfortunately, the Sarama client for Kafka 1.0.0 does not work with the mock broker, so this change breaks our tests. To repair the situation, we specify that the client should use version 0.8.2. If Sarama fixes their mock client in the future, we can use the default version instead. --- contrib/Shopify/sarama/sarama_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contrib/Shopify/sarama/sarama_test.go b/contrib/Shopify/sarama/sarama_test.go index 5382642de1..d7adc0e789 100644 --- a/contrib/Shopify/sarama/sarama_test.go +++ b/contrib/Shopify/sarama/sarama_test.go @@ -36,8 +36,9 @@ func TestConsumer(t *testing.T) { SetMessage("test-topic", 0, 0, sarama.StringEncoder("hello")). SetMessage("test-topic", 0, 1, sarama.StringEncoder("world")), }) - - client, err := sarama.NewClient([]string{broker.Addr()}, sarama.NewConfig()) + cfg := sarama.NewConfig() + cfg.Version = sarama.MinVersion + client, err := sarama.NewClient([]string{broker.Addr()}, cfg) if err != nil { t.Fatal(err) } @@ -113,6 +114,7 @@ func TestSyncProducer(t *testing.T) { leader.Returns(prodSuccess) cfg := sarama.NewConfig() + cfg.Version = sarama.MinVersion cfg.Producer.Return.Successes = true producer, err := sarama.NewSyncProducer([]string{seedBroker.Addr()}, cfg) @@ -160,6 +162,7 @@ func TestSyncProducerSendMessages(t *testing.T) { leader.Returns(prodSuccess) cfg := sarama.NewConfig() + cfg.Version = sarama.MinVersion cfg.Producer.Return.Successes = true cfg.Producer.Flush.Messages = 2 @@ -200,7 +203,9 @@ func TestAsyncProducer(t *testing.T) { broker := newMockBroker(t) - producer, err := sarama.NewAsyncProducer([]string{broker.Addr()}, nil) + cfg := sarama.NewConfig() + cfg.Version = sarama.MinVersion + producer, err := sarama.NewAsyncProducer([]string{broker.Addr()}, cfg) if err != nil { t.Fatal(err) } @@ -234,6 +239,7 @@ func TestAsyncProducer(t *testing.T) { broker := newMockBroker(t) cfg := sarama.NewConfig() + cfg.Version = sarama.MinVersion cfg.Producer.Return.Successes = true producer, err := sarama.NewAsyncProducer([]string{broker.Addr()}, cfg) From 977bbb89f54010b920264aeda051b5a67f46c26a Mon Sep 17 00:00:00 2001 From: Makoto Tajitsu Date: Thu, 15 Oct 2020 23:10:11 +0900 Subject: [PATCH 22/22] contrib/jmoiron/sqlx: allow adding sqltrace.Option to Open (#762) --- contrib/jmoiron/sqlx/sql.go | 4 ++-- contrib/jmoiron/sqlx/sql_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/contrib/jmoiron/sqlx/sql.go b/contrib/jmoiron/sqlx/sql.go index 3269d89115..d527fd54a1 100644 --- a/contrib/jmoiron/sqlx/sql.go +++ b/contrib/jmoiron/sqlx/sql.go @@ -20,8 +20,8 @@ import ( // Open opens a new (traced) connection to the database using the given driver and source. // Note that the driver must formerly be registered using database/sql integration's Register. -func Open(driverName, dataSourceName string) (*sqlx.DB, error) { - db, err := sqltraced.Open(driverName, dataSourceName) +func Open(driverName, dataSourceName string, opts ...sqltraced.Option) (*sqlx.DB, error) { + db, err := sqltraced.Open(driverName, dataSourceName, opts...) if err != nil { return nil, err } diff --git a/contrib/jmoiron/sqlx/sql_test.go b/contrib/jmoiron/sqlx/sql_test.go index d89ed4f3be..9061dbdfb6 100644 --- a/contrib/jmoiron/sqlx/sql_test.go +++ b/contrib/jmoiron/sqlx/sql_test.go @@ -81,3 +81,28 @@ func TestPostgres(t *testing.T) { } sqltest.RunAll(t, testConfig) } + +func TestOpenWithOptions(t *testing.T) { + sqltrace.Register("mysql", &mysql.MySQLDriver{}, sqltrace.WithServiceName("mysql-test")) + dbx, err := Open("mysql", "test:test@tcp(127.0.0.1:3306)/test", sqltrace.WithServiceName("other-service")) + if err != nil { + log.Fatal(err) + } + defer dbx.Close() + + testConfig := &sqltest.Config{ + DB: dbx.DB, + DriverName: "mysql", + TableName: tableName, + ExpectName: "mysql.query", + ExpectTags: map[string]interface{}{ + ext.ServiceName: "other-service", + ext.SpanType: ext.SpanTypeSQL, + ext.TargetHost: "127.0.0.1", + ext.TargetPort: "3306", + ext.DBUser: "test", + ext.DBName: "test", + }, + } + sqltest.RunAll(t, testConfig) +}