Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
7 changes: 6 additions & 1 deletion contrib/gin-gonic/gin/gintrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package gin // import "github.com/DataDog/dd-trace-go/contrib/gin-gonic/gin/v2"

import (
"errors"
"fmt"
"math"

Expand Down Expand Up @@ -53,7 +54,11 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc {
opts = append(opts, httptrace.HeaderTagsFromRequest(c.Request, cfg.headerTags))
span, ctx, finishSpans := httptrace.StartRequestSpan(c.Request, opts...)
defer func() {
finishSpans(c.Writer.Status(), nil)
status := c.Writer.Status()
if cfg.useGinErrors && cfg.isStatusError(status) && len(c.Errors) > 0 {
finishSpans(status, cfg.isStatusError, tracer.WithError(errors.New(c.Errors.String())))
}
finishSpans(status, cfg.isStatusError)
}()

// pass the span through the request context
Expand Down
81 changes: 77 additions & 4 deletions contrib/gin-gonic/gin/gintrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,84 @@ func TestError(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

// setup
router := gin.New()
router.Use(Middleware("foobar"))
responseErr := errors.New("oh no")

t.Run("server error", func(*testing.T) {
t.Run("server error - with error propagation", func(*testing.T) {
defer mt.Reset()

router := gin.New()
router.Use(Middleware("foobar", WithUseGinErrors()))

// configure a handler that returns an error and 5xx status code
router.GET("/server_err", func(c *gin.Context) {
c.AbortWithError(500, responseErr)
})
r := httptest.NewRequest("GET", "/server_err", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
defer response.Body.Close()
assert.Equal(response.StatusCode, 500)

// verify the errors and status are correct
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("foobar", span.Tag(ext.ServiceName))
assert.Equal("500", span.Tag(ext.HTTPCode))
assert.Equal(fmt.Sprintf("Error #01: %s\n", responseErr), span.Tag("gin.errors"))
// server errors set the ext.ErrorMsg tag
assert.Equal(fmt.Sprintf("Error #01: %s\n", responseErr), span.Tag(ext.ErrorMsg))
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
assert.Equal("gin-gonic/gin", span.Tag(ext.Component))
assert.Equal(componentName, span.Integration())
})

t.Run("server error - with error propagation - nil Errors in gin context", func(*testing.T) {
defer mt.Reset()

router := gin.New()
router.Use(Middleware("foobar", WithUseGinErrors()))

// configure a handler that returns an error and 5xx status code
router.GET("/server_err", func(c *gin.Context) {
c.AbortWithStatus(500)
})
r := httptest.NewRequest("GET", "/server_err", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
defer response.Body.Close()
assert.Equal(response.StatusCode, 500)

// verify the errors and status are correct
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("foobar", span.Tag(ext.ServiceName))
assert.Equal("500", span.Tag(ext.HTTPCode))
assert.Empty(span.Tag("gin.errors"))
// server errors set the ext.ErrorMsg tag
assert.Equal("500: Internal Server Error", span.Tag(ext.ErrorMsg))
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
assert.Equal("gin-gonic/gin", span.Tag(ext.Component))
assert.Equal(componentName, span.Integration())
})

t.Run("server error - without error propagation", func(*testing.T) {
defer mt.Reset()

router := gin.New()
router.Use(Middleware("foobar"))

// configure a handler that returns an error and 5xx status code
router.GET("/server_err", func(c *gin.Context) {
c.AbortWithError(500, responseErr)
Expand Down Expand Up @@ -219,6 +289,9 @@ func TestError(t *testing.T) {
t.Run("client error", func(*testing.T) {
defer mt.Reset()

router := gin.New()
router.Use(Middleware("foobar"))

// configure a handler that returns an error and 4xx status code
router.GET("/client_err", func(c *gin.Context) {
c.AbortWithError(418, responseErr)
Expand Down
24 changes: 24 additions & 0 deletions contrib/gin-gonic/gin/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type config struct {
resourceNamer func(c *gin.Context) string
serviceName string
ignoreRequest func(c *gin.Context) bool
isStatusError func(statusCode int) bool
useGinErrors bool
headerTags instrumentation.HeaderTags
}

Expand All @@ -32,6 +34,8 @@ func newConfig(serviceName string) *config {
resourceNamer: defaultResourceNamer,
serviceName: serviceName,
ignoreRequest: func(_ *gin.Context) bool { return false },
isStatusError: isServerError,
useGinErrors: false,
headerTags: instr.HTTPHeadersAsTags(),
}
}
Expand Down Expand Up @@ -79,6 +83,26 @@ func WithResourceNamer(namer func(c *gin.Context) string) OptionFn {
}
}

// WithStatusCheck specifies a function fn which reports whether the passed
// statusCode should be considered an error.
func WithStatusCheck(fn func(statusCode int) bool) OptionFn {
return func(cfg *config) {
cfg.isStatusError = fn
}
}

func isServerError(statusCode int) bool {
return statusCode >= 500 && statusCode < 600
}

// WithUseGinErrors enables the usage of gin's errors for the span instead of crafting generic errors from the status code.
// If there are multiple errors in the gin context, they will be all added to the span.
func WithUseGinErrors() OptionFn {
return func(cfg *config) {
cfg.useGinErrors = true
}
}

// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
// Warning:
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.
Expand Down
Loading