diff --git a/CHANGELOG.md b/CHANGELOG.md index 479077cfbde..f77597142e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108) +- Add `WithSpanStartOption` and `WithSpanEndOption` to the `go.opentelemetry.io/contrib/github.com/gorilla/mux/otelmux` package to provide options on span start and end. (#5250) ### Removed diff --git a/instrumentation/github.com/gorilla/mux/otelmux/config.go b/instrumentation/github.com/gorilla/mux/otelmux/config.go index 2900924838b..c8ae1401f12 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/config.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/config.go @@ -18,6 +18,8 @@ type config struct { PublicEndpoint bool PublicEndpointFn func(*http.Request) bool Filters []Filter + SpanStartOptions []oteltrace.SpanStartOption + SpanEndOptions []oteltrace.SpanEndOption } // Option specifies instrumentation configuration options. @@ -86,6 +88,22 @@ func WithSpanNameFormatter(fn func(routeName string, r *http.Request) string) Op }) } +// WithSpanStartOption applies options to all the HTTP span created by the +// instrumentation. +func WithSpanStartOption(o ...oteltrace.SpanStartOption) Option { + return optionFunc(func(c *config) { + c.SpanStartOptions = append(c.SpanStartOptions, o...) + }) +} + +// WithSpanEndOption applies options when ending the HTTP span created by the +// instrumentation. +func WithSpanEndOption(o ...oteltrace.SpanEndOption) Option { + return optionFunc(func(c *config) { + c.SpanEndOptions = append(c.SpanEndOptions, o...) + }) +} + // 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. diff --git a/instrumentation/github.com/gorilla/mux/otelmux/mux.go b/instrumentation/github.com/gorilla/mux/otelmux/mux.go index c7b2355eca8..a973f39310f 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/mux.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/mux.go @@ -55,6 +55,8 @@ func Middleware(service string, opts ...Option) mux.MiddlewareFunc { publicEndpoint: cfg.PublicEndpoint, publicEndpointFn: cfg.PublicEndpointFn, filters: cfg.Filters, + SpanStartOptions: cfg.SpanStartOptions, + SpanEndOptions: cfg.SpanEndOptions, } } } @@ -68,6 +70,8 @@ type traceware struct { publicEndpoint bool publicEndpointFn func(*http.Request) bool filters []Filter + SpanStartOptions []trace.SpanStartOption + SpanEndOptions []trace.SpanEndOption } type recordingResponseWriter struct { @@ -141,10 +145,10 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - opts := []trace.SpanStartOption{ + opts := append(tw.SpanStartOptions, []trace.SpanStartOption{ trace.WithAttributes(semconvutil.HTTPServerRequest(tw.service, r)...), trace.WithSpanKind(trace.SpanKindServer), - } + }...) if tw.publicEndpoint || (tw.publicEndpointFn != nil && tw.publicEndpointFn(r.WithContext(ctx))) { opts = append(opts, trace.WithNewRoot()) @@ -162,7 +166,7 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { } spanName := tw.spanNameFormatter(routeStr, r) ctx, span := tw.tracer.Start(ctx, spanName, opts...) - defer span.End() + defer span.End(tw.SpanEndOptions...) r2 := r.WithContext(ctx) rrw := getRRW(w) defer putRRW(rrw) diff --git a/instrumentation/github.com/gorilla/mux/otelmux/test/mux_test.go b/instrumentation/github.com/gorilla/mux/otelmux/test/mux_test.go index 13e900b5ca1..108bf4fb326 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/test/mux_test.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/test/mux_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" @@ -282,3 +283,67 @@ func TestWithPublicEndpointFn(t *testing.T) { }) } } + +func TestWithSpanStartOptions(t *testing.T) { + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + // Setup + router := mux.NewRouter() + router.Use(otelmux.Middleware("foobar", + otelmux.WithTracerProvider(provider), + otelmux.WithSpanStartOption( + trace.WithAttributes(attribute.String("spanStart", "true")), + ), + )) + + // Configure an empty handler + router.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + }) + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + response := w.Result() + assert.Equal(t, http.StatusOK, response.StatusCode) + + // Verify that the attribute is set as expected + spans := sr.Ended() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal(t, "/", span.Name()) + attr := span.Attributes() + assert.Contains(t, attr, attribute.String("spanStart", "true")) +} + +func TestWithSpanEndOptions(t *testing.T) { + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + // Setup + router := mux.NewRouter() + endTime := time.Now() + router.Use(otelmux.Middleware("foobar", + otelmux.WithTracerProvider(provider), + otelmux.WithSpanEndOption( + trace.WithTimestamp(endTime), + ), + )) + + // Configure an empty handler + router.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + }) + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + response := w.Result() + assert.Equal(t, http.StatusOK, response.StatusCode) + + // Verify that the attribute is set as expected + spans := sr.Ended() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal(t, "/", span.Name()) + + // Assert that the time set in the SpanEndOptions above matches the EndTime of the span + assert.Equal(t, span.EndTime(), endTime) +}