diff --git a/contrib/gin-gonic/gin/gintrace.go b/contrib/gin-gonic/gin/gintrace.go index 5db5d31d0a..d4f66befbd 100644 --- a/contrib/gin-gonic/gin/gintrace.go +++ b/contrib/gin-gonic/gin/gintrace.go @@ -7,6 +7,7 @@ package gin // import "github.com/DataDog/dd-trace-go/contrib/gin-gonic/gin/v2" import ( + "errors" "fmt" "math" @@ -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 diff --git a/contrib/gin-gonic/gin/gintrace_test.go b/contrib/gin-gonic/gin/gintrace_test.go index c605938cae..c8454df4ec 100644 --- a/contrib/gin-gonic/gin/gintrace_test.go +++ b/contrib/gin-gonic/gin/gintrace_test.go @@ -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) @@ -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) diff --git a/contrib/gin-gonic/gin/option.go b/contrib/gin-gonic/gin/option.go index e0b98f9861..da97f95fd3 100644 --- a/contrib/gin-gonic/gin/option.go +++ b/contrib/gin-gonic/gin/option.go @@ -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 } @@ -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(), } } @@ -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.