Skip to content

Commit 73258cd

Browse files
theo303rarguelloFdarccio
authored
feat(contrib/gin-gonic/gin): add WithStatusCheck and WithUseGinErrors options (#3984)
Co-authored-by: rarguelloF <[email protected]> Co-authored-by: dario.castane <[email protected]>
1 parent 2a58e9f commit 73258cd

File tree

3 files changed

+121
-8
lines changed

3 files changed

+121
-8
lines changed

contrib/gin-gonic/gin/gintrace.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package gin // import "github.com/DataDog/dd-trace-go/contrib/gin-gonic/gin/v2"
88

99
import (
10+
"errors"
1011
"fmt"
1112
"math"
1213

@@ -53,7 +54,11 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc {
5354
opts = append(opts, httptrace.HeaderTagsFromRequest(c.Request, cfg.headerTags))
5455
span, ctx, finishSpans := httptrace.StartRequestSpan(c.Request, opts...)
5556
defer func() {
56-
finishSpans(c.Writer.Status(), nil)
57+
status := c.Writer.Status()
58+
if cfg.useGinErrors && cfg.isStatusError(status) && len(c.Errors) > 0 {
59+
finishSpans(status, cfg.isStatusError, tracer.WithError(errors.New(c.Errors.String())))
60+
}
61+
finishSpans(status, cfg.isStatusError)
5762
}()
5863

5964
// pass the span through the request context

contrib/gin-gonic/gin/gintrace_test.go

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,84 @@ func TestError(t *testing.T) {
179179
mt := mocktracer.Start()
180180
defer mt.Stop()
181181

182-
// setup
183-
router := gin.New()
184-
router.Use(Middleware("foobar"))
185182
responseErr := errors.New("oh no")
186183

187-
t.Run("server error", func(*testing.T) {
184+
t.Run("server error - with error propagation", func(*testing.T) {
185+
defer mt.Reset()
186+
187+
router := gin.New()
188+
router.Use(Middleware("foobar", WithUseGinErrors()))
189+
190+
// configure a handler that returns an error and 5xx status code
191+
router.GET("/server_err", func(c *gin.Context) {
192+
c.AbortWithError(500, responseErr)
193+
})
194+
r := httptest.NewRequest("GET", "/server_err", nil)
195+
w := httptest.NewRecorder()
196+
router.ServeHTTP(w, r)
197+
response := w.Result()
198+
defer response.Body.Close()
199+
assert.Equal(response.StatusCode, 500)
200+
201+
// verify the errors and status are correct
202+
spans := mt.FinishedSpans()
203+
assert.Len(spans, 1)
204+
if len(spans) < 1 {
205+
t.Fatalf("no spans")
206+
}
207+
span := spans[0]
208+
assert.Equal("http.request", span.OperationName())
209+
assert.Equal("foobar", span.Tag(ext.ServiceName))
210+
assert.Equal("500", span.Tag(ext.HTTPCode))
211+
assert.Equal(fmt.Sprintf("Error #01: %s\n", responseErr), span.Tag("gin.errors"))
212+
// server errors set the ext.ErrorMsg tag
213+
assert.Equal(fmt.Sprintf("Error #01: %s\n", responseErr), span.Tag(ext.ErrorMsg))
214+
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
215+
assert.Equal("gin-gonic/gin", span.Tag(ext.Component))
216+
assert.Equal(componentName, span.Integration())
217+
})
218+
219+
t.Run("server error - with error propagation - nil Errors in gin context", func(*testing.T) {
220+
defer mt.Reset()
221+
222+
router := gin.New()
223+
router.Use(Middleware("foobar", WithUseGinErrors()))
224+
225+
// configure a handler that returns an error and 5xx status code
226+
router.GET("/server_err", func(c *gin.Context) {
227+
c.AbortWithStatus(500)
228+
})
229+
r := httptest.NewRequest("GET", "/server_err", nil)
230+
w := httptest.NewRecorder()
231+
router.ServeHTTP(w, r)
232+
response := w.Result()
233+
defer response.Body.Close()
234+
assert.Equal(response.StatusCode, 500)
235+
236+
// verify the errors and status are correct
237+
spans := mt.FinishedSpans()
238+
assert.Len(spans, 1)
239+
if len(spans) < 1 {
240+
t.Fatalf("no spans")
241+
}
242+
span := spans[0]
243+
assert.Equal("http.request", span.OperationName())
244+
assert.Equal("foobar", span.Tag(ext.ServiceName))
245+
assert.Equal("500", span.Tag(ext.HTTPCode))
246+
assert.Empty(span.Tag("gin.errors"))
247+
// server errors set the ext.ErrorMsg tag
248+
assert.Equal("500: Internal Server Error", span.Tag(ext.ErrorMsg))
249+
assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind))
250+
assert.Equal("gin-gonic/gin", span.Tag(ext.Component))
251+
assert.Equal(componentName, span.Integration())
252+
})
253+
254+
t.Run("server error - without error propagation", func(*testing.T) {
188255
defer mt.Reset()
189256

257+
router := gin.New()
258+
router.Use(Middleware("foobar"))
259+
190260
// configure a handler that returns an error and 5xx status code
191261
router.GET("/server_err", func(c *gin.Context) {
192262
c.AbortWithError(500, responseErr)
@@ -219,6 +289,9 @@ func TestError(t *testing.T) {
219289
t.Run("client error", func(*testing.T) {
220290
defer mt.Reset()
221291

292+
router := gin.New()
293+
router.Use(Middleware("foobar"))
294+
222295
// configure a handler that returns an error and 4xx status code
223296
router.GET("/client_err", func(c *gin.Context) {
224297
c.AbortWithError(418, responseErr)

contrib/gin-gonic/gin/option.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,43 @@ import (
1212
"github.com/gin-gonic/gin"
1313

1414
"github.com/DataDog/dd-trace-go/v2/instrumentation"
15+
"github.com/DataDog/dd-trace-go/v2/instrumentation/env"
16+
"github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace"
1517
)
1618

19+
// envServerErrorStatuses is the name of the env var used to specify error status codes on http server spans
20+
const envServerErrorStatuses = "DD_TRACE_HTTP_SERVER_ERROR_STATUSES"
21+
1722
type config struct {
1823
analyticsRate float64
1924
resourceNamer func(c *gin.Context) string
2025
serviceName string
2126
ignoreRequest func(c *gin.Context) bool
27+
isStatusError func(statusCode int) bool
28+
useGinErrors bool
2229
headerTags instrumentation.HeaderTags
2330
}
2431

2532
func newConfig(serviceName string) *config {
2633
if serviceName == "" {
2734
serviceName = instr.ServiceName(instrumentation.ComponentServer, nil)
2835
}
29-
rate := instr.AnalyticsRate(true)
30-
return &config{
31-
analyticsRate: rate,
36+
cfg := &config{
37+
analyticsRate: instr.AnalyticsRate(true),
3238
resourceNamer: defaultResourceNamer,
3339
serviceName: serviceName,
3440
ignoreRequest: func(_ *gin.Context) bool { return false },
41+
useGinErrors: false,
3542
headerTags: instr.HTTPHeadersAsTags(),
3643
}
44+
45+
if fn := httptrace.GetErrorCodesFromInput(env.Get(envServerErrorStatuses)); fn != nil {
46+
cfg.isStatusError = fn
47+
} else {
48+
cfg.isStatusError = isServerError
49+
}
50+
51+
return cfg
3752
}
3853

3954
// Option describes options for the Gin integration.
@@ -79,6 +94,26 @@ func WithResourceNamer(namer func(c *gin.Context) string) OptionFn {
7994
}
8095
}
8196

97+
// WithStatusCheck specifies a function fn which reports whether the passed
98+
// statusCode should be considered an error.
99+
func WithStatusCheck(fn func(statusCode int) bool) OptionFn {
100+
return func(cfg *config) {
101+
cfg.isStatusError = fn
102+
}
103+
}
104+
105+
func isServerError(statusCode int) bool {
106+
return statusCode >= 500 && statusCode < 600
107+
}
108+
109+
// WithUseGinErrors enables the usage of gin's errors for the span instead of crafting generic errors from the status code.
110+
// If there are multiple errors in the gin context, they will be all added to the span.
111+
func WithUseGinErrors() OptionFn {
112+
return func(cfg *config) {
113+
cfg.useGinErrors = true
114+
}
115+
}
116+
82117
// WithHeaderTags enables the integration to attach HTTP request headers as span tags.
83118
// Warning:
84119
// Using this feature can risk exposing sensitive data such as authorization tokens to Datadog.

0 commit comments

Comments
 (0)