diff --git a/CHANGELOG.md b/CHANGELOG.md index 11378f5918e..4cf4495e0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `WithLogger` option to `go.opentelemetry.io/contrib/samplers/jaegerremote` to allow users to pass a `logr.Logger` and have operations logged. (#2566) - Add the `messaging.url` & `messaging.system` attributes to all appropriate SQS operations in the `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` package. (#2879) - Add example use of the metrics signal to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/example`. (#2610) +- [otelgin] Add support for filters to the `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` package to provide the way to control which inbound requests are traced. (#2963) ### Fixed diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go index dce7e4ae8aa..1c2ce785331 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go @@ -54,6 +54,14 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { cfg.Propagators = otel.GetTextMapPropagator() } return func(c *gin.Context) { + for _, f := range cfg.Filters { + if !f(c.Request) { + // 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 2c3eb74913e..d8e1d1d9e3d 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go @@ -17,6 +17,8 @@ package otelgin // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" import ( + "net/http" + "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -24,8 +26,13 @@ import ( type config struct { TracerProvider oteltrace.TracerProvider Propagators propagation.TextMapPropagator + Filters []Filter } +// Filter is a predicate used to determine whether a given http.request should +// be traced. A Filter must return true if the request should be traced. +type Filter func(*http.Request) bool + // Option specifies instrumentation configuration options. type Option interface { apply(*config) @@ -57,3 +64,15 @@ 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. +// 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. +func WithFilter(f ...Filter) Option { + return optionFunc(func(c *config) { + c.Filters = append(c.Filters, f...) + }) +} 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 d67702353f6..b430c5c8cf7 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 @@ -203,3 +203,37 @@ func TestHTML(t *testing.T) { require.NotNil(t, tspan) assert.Contains(t, tspan.Attributes(), attribute.String("go.template", "hello")) } + +func TestWithFilter(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(req *http.Request) bool { return req.URL.Path != "/healthcheck" } + router.Use(otelgin.Middleware("foobar", otelgin.WithFilter(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(req *http.Request) bool { return req.URL.Path != "/healthcheck" } + router.Use(otelgin.Middleware("foobar", otelgin.WithFilter(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) + }) +}