diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa0cfb9940..8308e1fd79b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `WithRouteTag` in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` adds HTTP route attribute to metrics. (#615) - Add `WithSpanOptions` option in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`. (#3768) - Add testing support for Go 1.21. (#4233) +- Add `WithFilter` option to `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux`. (#4230) ### Changed diff --git a/instrumentation/github.com/gorilla/mux/otelmux/config.go b/instrumentation/github.com/gorilla/mux/otelmux/config.go index 391ee067b80..c3afabf3c86 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/config.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/config.go @@ -28,6 +28,7 @@ type config struct { spanNameFormatter func(string, *http.Request) string PublicEndpoint bool PublicEndpointFn func(*http.Request) bool + Filters []Filter } // Option specifies instrumentation configuration options. @@ -41,6 +42,10 @@ func (o optionFunc) apply(c *config) { o(c) } +// 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 + // WithPublicEndpoint configures the Handler to link the span with an incoming // span context. If this option is not provided, then the association is a child // association instead of a link. @@ -91,3 +96,15 @@ func WithSpanNameFormatter(fn func(routeName string, r *http.Request) string) Op cfg.spanNameFormatter = fn }) } + +// 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/gorilla/mux/otelmux/mux.go b/instrumentation/github.com/gorilla/mux/otelmux/mux.go index cf0d8f83f8f..f1ead848b3b 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/mux.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/mux.go @@ -64,6 +64,7 @@ func Middleware(service string, opts ...Option) mux.MiddlewareFunc { spanNameFormatter: cfg.spanNameFormatter, publicEndpoint: cfg.PublicEndpoint, publicEndpointFn: cfg.PublicEndpointFn, + filters: cfg.Filters, } } } @@ -76,6 +77,7 @@ type traceware struct { spanNameFormatter func(string, *http.Request) string publicEndpoint bool publicEndpointFn func(*http.Request) bool + filters []Filter } type recordingResponseWriter struct { @@ -127,6 +129,14 @@ func defaultSpanNameFunc(routeName string, _ *http.Request) string { return rout // ServeHTTP implements the http.Handler interface. It does the actual // tracing of the request. func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + for _, f := range tw.filters { + if !f(r) { + // Simply pass through to the handler if a filter rejects the request + tw.handler.ServeHTTP(w, r) + return + } + } + ctx := tw.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) routeStr := "" route := mux.CurrentRoute(r) diff --git a/instrumentation/github.com/gorilla/mux/otelmux/mux_test.go b/instrumentation/github.com/gorilla/mux/otelmux/mux_test.go index b9e9a3eb46e..b7823d7ce6c 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/mux_test.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/mux_test.go @@ -162,3 +162,40 @@ func TestResponseWriterInterfaces(t *testing.T) { router.ServeHTTP(w, r) } + +func TestFilter(t *testing.T) { + prop := propagation.TraceContext{} + + router := mux.NewRouter() + var calledHealth, calledTest int + router.Use(Middleware("foobar", WithFilter(func(r *http.Request) bool { + return r.URL.Path != "/health" + }))) + router.HandleFunc("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calledHealth++ + span := trace.SpanFromContext(r.Context()) + assert.NotEqual(t, sc, span.SpanContext()) + w.WriteHeader(http.StatusOK) + })) + router.HandleFunc("/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calledTest++ + span := trace.SpanFromContext(r.Context()) + assert.Equal(t, sc, span.SpanContext()) + w.WriteHeader(http.StatusOK) + })) + + r := httptest.NewRequest("GET", "/health", nil) + ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc) + prop.Inject(ctx, propagation.HeaderCarrier(r.Header)) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + r = httptest.NewRequest("GET", "/test", nil) + ctx = trace.ContextWithRemoteSpanContext(context.Background(), sc) + prop.Inject(ctx, propagation.HeaderCarrier(r.Header)) + w = httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(t, 1, calledHealth, "failed to run test") + assert.Equal(t, 1, calledTest, "failed to run test") +}