Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib/labstack/echo.v4: add support for echo@v4 #698

Merged
merged 1 commit into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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