diff --git a/CHANGELOG.md b/CHANGELOG.md index 41521bc3d77..edbd15e3883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Support for the `OTEL_HTTP_CLIENT_COMPATIBILITY_MODE=http/dup` environment variable in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` to emit attributes for both the v1.20.0 and v1.26.0 semantic conventions. (#5401) - The `go.opentelemetry.io/contrib/bridges/otelzerolog` module. This module provides an OpenTelemetry logging bridge for `github.com/rs/zerolog`. (#5405) +- Add `WithGinFilter` filter parameter in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` to allow filtering requests with `*gin.Context`. (#5743) ### Removed diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go index e40775d7c3e..1affd4d6ca5 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go @@ -52,6 +52,14 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { return } } + for _, f := range cfg.GinFilters { + if !f(c) { + // Serve the request to the next middleware + // if a filter rejects the request. + c.Next() + return + } + } c.Set(tracerKey, tracer) savedCtx := c.Request.Context() defer func() { diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go index 113576ca00a..143ca8e849e 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go @@ -8,6 +8,8 @@ package otelgin // import "go.opentelemetry.io/contrib/instrumentation/github.co import ( "net/http" + "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -16,6 +18,7 @@ type config struct { TracerProvider oteltrace.TracerProvider Propagators propagation.TextMapPropagator Filters []Filter + GinFilters []GinFilter SpanNameFormatter SpanNameFormatter } @@ -23,6 +26,10 @@ type config struct { // be traced. A Filter must return true if the request should be traced. type Filter func(*http.Request) bool +// Adding new Filter parameter (*gin.Context) +// gin.Context has FullPath() method, which returns a matched route full path. +type GinFilter func(*gin.Context) bool + // SpanNameFormatter is used to set span name by http.request. type SpanNameFormatter func(r *http.Request) string @@ -60,7 +67,7 @@ func WithTracerProvider(provider oteltrace.TracerProvider) Option { // WithFilter adds a filter to the list of filters used by the handler. // If any filter indicates to exclude a request then the request will not be -// traced. All filters must allow a request to be traced for a Span to be created. +// traced. All gin and net/http filters must allow a request to be traced for a Span to be created. // If no filters are provided then all requests are traced. // Filters will be invoked for each processed request, it is advised to make them // simple and fast. @@ -70,6 +77,13 @@ func WithFilter(f ...Filter) Option { }) } +// WithGinFilter adds a gin filter to the list of filters used by the handler. +func WithGinFilter(f ...GinFilter) Option { + return optionFunc(func(c *config) { + c.GinFilters = append(c.GinFilters, f...) + }) +} + // WithSpanNameFormatter takes a function that will be called on every // request and the returned string will become the Span Name. func WithSpanNameFormatter(f func(r *http.Request) string) Option { diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go index 162f59d988f..d90ac7ec9c0 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go @@ -287,3 +287,37 @@ func TestWithFilter(t *testing.T) { assert.Len(t, sr.Ended(), 1) }) } + +func TestWithGinFilter(t *testing.T) { + t.Run("custom filter filtering route", func(t *testing.T) { + sr := tracetest.NewSpanRecorder() + otel.SetTracerProvider(sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))) + + router := gin.New() + f := func(c *gin.Context) bool { return c.Request.URL.Path != "/healthcheck" } + router.Use(otelgin.Middleware("foobar", otelgin.WithGinFilter(f))) + router.GET("/healthcheck", func(c *gin.Context) {}) + + r := httptest.NewRequest("GET", "/healthcheck", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) + assert.Len(t, sr.Ended(), 0) + }) + + t.Run("custom filter not filtering route", func(t *testing.T) { + sr := tracetest.NewSpanRecorder() + otel.SetTracerProvider(sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))) + + router := gin.New() + f := func(c *gin.Context) bool { return c.Request.URL.Path != "/user/:id" } + router.Use(otelgin.Middleware("foobar", otelgin.WithGinFilter(f))) + router.GET("/user/:id", func(c *gin.Context) {}) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) + assert.Len(t, sr.Ended(), 1) + }) +}