diff --git a/contrib/labstack/echo.v4/echotrace.go b/contrib/labstack/echo.v4/echotrace.go new file mode 100644 index 0000000000..85ee2d8edf --- /dev/null +++ b/contrib/labstack/echo.v4/echotrace.go @@ -0,0 +1,64 @@ +// 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 echo provides functions to trace the labstack/echo package (https://github.com/labstack/echo). +package echo + +import ( + "math" + "strconv" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "github.com/labstack/echo/v4" +) + +// Middleware returns echo middleware which will trace incoming requests. +func Middleware(opts ...Option) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + cfg := new(config) + defaults(cfg) + for _, fn := range opts { + fn(cfg) + } + return func(c echo.Context) error { + request := c.Request() + resource := request.Method + " " + c.Path() + opts := []ddtrace.StartSpanOption{ + tracer.ServiceName(cfg.serviceName), + tracer.ResourceName(resource), + tracer.SpanType(ext.SpanTypeWeb), + tracer.Tag(ext.HTTPMethod, request.Method), + tracer.Tag(ext.HTTPURL, request.URL.Path), + tracer.Measured(), + } + + if !math.IsNaN(cfg.analyticsRate) { + opts = append(opts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) + } + if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(request.Header)); err == nil { + opts = append(opts, tracer.ChildOf(spanctx)) + } + span, ctx := tracer.StartSpanFromContext(request.Context(), "http.request", opts...) + defer span.Finish() + + // pass the span through the request context + c.SetRequest(request.WithContext(ctx)) + + // serve the request to the next middleware + err := next(c) + if err != nil { + span.SetTag(ext.Error, err) + // invokes the registered HTTP error handler + c.Error(err) + } + + span.SetTag(ext.HTTPCode, strconv.Itoa(c.Response().Status)) + return err + } + } +} diff --git a/contrib/labstack/echo.v4/echotrace_test.go b/contrib/labstack/echo.v4/echotrace_test.go new file mode 100644 index 0000000000..406cc612da --- /dev/null +++ b/contrib/labstack/echo.v4/echotrace_test.go @@ -0,0 +1,232 @@ +// 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 echo + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "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" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestChildSpan(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + var called, traced bool + + router := echo.New() + router.Use(Middleware(WithServiceName("foobar"))) + router.GET("/user/:id", func(c echo.Context) error { + called = true + _, traced = tracer.SpanFromContext(c.Request().Context()) + return c.NoContent(200) + }) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // verify traces look good + assert.True(called) + assert.True(traced) +} + +func TestTrace200(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + var called, traced bool + + router := echo.New() + router.Use(Middleware(WithServiceName("foobar"), WithAnalytics(false))) + router.GET("/user/:id", func(c echo.Context) error { + called = true + var span tracer.Span + span, traced = tracer.SpanFromContext(c.Request().Context()) + + // we patch the span on the request context. + span.SetTag("test.echo", "echony") + assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "foobar") + return c.NoContent(200) + }) + + root := tracer.StartSpan("root") + r := httptest.NewRequest("GET", "/user/123", nil) + err := tracer.Inject(root.Context(), tracer.HTTPHeadersCarrier(r.Header)) + assert.Nil(err) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // verify traces look good + assert.True(called) + assert.True(traced) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + 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("echony", span.Tag("test.echo")) + assert.Contains(span.Tag(ext.ResourceName), "/user/:id") + assert.Equal("200", span.Tag(ext.HTTPCode)) + assert.Equal("GET", span.Tag(ext.HTTPMethod)) + assert.Equal(root.Context().SpanID(), span.ParentID()) + + assert.Equal("/user/123", span.Tag(ext.HTTPURL)) +} + +func TestTraceAnalytics(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + var called, traced bool + + router := echo.New() + router.Use(Middleware(WithServiceName("foobar"), WithAnalytics(true))) + router.GET("/user/:id", func(c echo.Context) error { + called = true + var span tracer.Span + span, traced = tracer.SpanFromContext(c.Request().Context()) + + // we patch the span on the request context. + span.SetTag("test.echo", "echony") + assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "foobar") + return c.NoContent(200) + }) + + root := tracer.StartSpan("root") + r := httptest.NewRequest("GET", "/user/123", nil) + err := tracer.Inject(root.Context(), tracer.HTTPHeadersCarrier(r.Header)) + assert.Nil(err) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // verify traces look good + assert.True(called) + assert.True(traced) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + 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("echony", span.Tag("test.echo")) + assert.Contains(span.Tag(ext.ResourceName), "/user/:id") + assert.Equal("200", span.Tag(ext.HTTPCode)) + assert.Equal("GET", span.Tag(ext.HTTPMethod)) + assert.Equal(1.0, span.Tag(ext.EventSampleRate)) + assert.Equal(root.Context().SpanID(), span.ParentID()) + + assert.Equal("/user/123", span.Tag(ext.HTTPURL)) +} + +func TestError(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + var called, traced bool + + // setup + router := echo.New() + router.Use(Middleware(WithServiceName("foobar"))) + wantErr := errors.New("oh no") + + // a handler with an error and make the requests + router.GET("/err", func(c echo.Context) error { + _, traced = tracer.SpanFromContext(c.Request().Context()) + called = true + + err := wantErr + c.Error(err) + return err + }) + r := httptest.NewRequest("GET", "/err", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // verify the errors and status are correct + assert.True(called) + assert.True(traced) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("http.request", span.OperationName()) + assert.Equal("foobar", span.Tag(ext.ServiceName)) + assert.Equal("500", span.Tag(ext.HTTPCode)) + assert.Equal(wantErr.Error(), span.Tag(ext.Error).(error).Error()) +} + +func TestErrorHandling(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + var called, traced bool + + // setup + router := echo.New() + router.HTTPErrorHandler = func(err error, ctx echo.Context) { + ctx.Response().WriteHeader(http.StatusInternalServerError) + } + router.Use(Middleware(WithServiceName("foobar"))) + wantErr := errors.New("oh no") + + // a handler with an error and make the requests + router.GET("/err", func(c echo.Context) error { + _, traced = tracer.SpanFromContext(c.Request().Context()) + called = true + return wantErr + }) + r := httptest.NewRequest("GET", "/err", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + // verify the errors and status are correct + assert.True(called) + assert.True(traced) + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + + span := spans[0] + assert.Equal("http.request", span.OperationName()) + assert.Equal("foobar", span.Tag(ext.ServiceName)) + assert.Equal("500", span.Tag(ext.HTTPCode)) + assert.Equal(wantErr.Error(), span.Tag(ext.Error).(error).Error()) +} + +func TestGetSpanNotInstrumented(t *testing.T) { + assert := assert.New(t) + router := echo.New() + var called, traced bool + + router.GET("/ping", func(c echo.Context) error { + // Assert we don't have a span on the context. + called = true + _, traced = tracer.SpanFromContext(c.Request().Context()) + return c.NoContent(200) + }) + + r := httptest.NewRequest("GET", "/ping", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) + assert.True(called) + assert.False(traced) +} diff --git a/contrib/labstack/echo.v4/example_test.go b/contrib/labstack/echo.v4/example_test.go new file mode 100644 index 0000000000..b5046182f6 --- /dev/null +++ b/contrib/labstack/echo.v4/example_test.go @@ -0,0 +1,50 @@ +// 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 echo + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "github.com/labstack/echo/v4" +) + +// To start tracing requests, add the trace middleware to your echo router. +func Example() { + r := echo.New() + + // Use the tracer middleware with your desired service name. + r.Use(Middleware(WithServiceName("my-web-app"))) + + // Set up an endpoint. + r.GET("/hello", func(c echo.Context) error { + return c.String(200, "hello world!") + }) + + // ...and listen for incoming requests + r.Start(":8080") +} + +// An example illustrating tracing a child operation within the main context. +func Example_spanFromContext() { + // Create a new instance of echo + r := echo.New() + + // Use the tracer middleware with your desired service name. + r.Use(Middleware(WithServiceName("image-encoder"))) + + // Set up some endpoints. + r.GET("/image/encode", func(c echo.Context) error { + // create a child span to track an operation + span, _ := tracer.StartSpanFromContext(c.Request().Context(), "image.encode") + + // encode an image ... + + // finish the child span + span.Finish() + + return c.String(200, "ok!") + }) +} diff --git a/contrib/labstack/echo.v4/option.go b/contrib/labstack/echo.v4/option.go new file mode 100644 index 0000000000..c9658e647d --- /dev/null +++ b/contrib/labstack/echo.v4/option.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 echo + +import ( + "math" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" +) + +type config struct { + serviceName string + analyticsRate float64 +} + +// Option represents an option that can be passed to Middleware. +type Option func(*config) + +func defaults(cfg *config) { + cfg.serviceName = "echo" + if svc := globalconfig.ServiceName(); svc != "" { + cfg.serviceName = svc + } + cfg.analyticsRate = math.NaN() +} + +// WithServiceName sets the given service name for the system. +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() + } + } +}