-
Notifications
You must be signed in to change notification settings - Fork 435
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
contrib/labstack/echo.v4: add support for echo@v4 (#698)
- Loading branch information
Showing
4 changed files
with
404 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") | ||
}) | ||
} |
Oops, something went wrong.