Skip to content

Commit

Permalink
contrib/labstack/echo.v4: add support for echo@v4 (#698)
Browse files Browse the repository at this point in the history
  • Loading branch information
knusbaum authored Jul 22, 2020
1 parent 9abaf0b commit bf1cbc5
Show file tree
Hide file tree
Showing 4 changed files with 404 additions and 0 deletions.
64 changes: 64 additions & 0 deletions contrib/labstack/echo.v4/echotrace.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
232 changes: 232 additions & 0 deletions contrib/labstack/echo.v4/echotrace_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
50 changes: 50 additions & 0 deletions contrib/labstack/echo.v4/example_test.go
Original file line number Diff line number Diff line change
@@ -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!")
})
}
Loading

0 comments on commit bf1cbc5

Please sign in to comment.