Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

otelmux: Add new WithSpanNameFormatter option #3041

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- `otelmux`: Add new `WithSpanNameFormatter` option to `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux` to allow customizing span names. (#3041)

## [1.12.0/0.37.0/0.6.0]

Expand Down
17 changes: 15 additions & 2 deletions instrumentation/github.com/gorilla/mux/otelmux/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
package otelmux // import "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"

import (
"net/http"

"go.opentelemetry.io/otel/propagation"
oteltrace "go.opentelemetry.io/otel/trace"
)

// config is used to configure the mux middleware.
type config struct {
TracerProvider oteltrace.TracerProvider
Propagators propagation.TextMapPropagator
TracerProvider oteltrace.TracerProvider
Propagators propagation.TextMapPropagator
spanNameFormatter func(string, *http.Request) string
}

// Option specifies instrumentation configuration options.
Expand Down Expand Up @@ -56,3 +59,13 @@ func WithTracerProvider(provider oteltrace.TracerProvider) Option {
}
})
}

// WithSpanNameFormatter specifies a function to use for generating a custom span
// name. By default, the route name (path template or regexp) is used. The route
// name is provided so you can use it in the span name without needing to
// duplicate the logic for extracting it from the request.
func WithSpanNameFormatter(fn func(routeName string, r *http.Request) string) Option {
hairyhenderson marked this conversation as resolved.
Show resolved Hide resolved
return optionFunc(func(cfg *config) {
cfg.spanNameFormatter = fn
})
}
2 changes: 2 additions & 0 deletions instrumentation/github.com/gorilla/mux/otelmux/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/stretchr/testify v1.8.1
go.opentelemetry.io/otel v1.11.2
go.opentelemetry.io/otel/sdk v1.11.2
go.opentelemetry.io/otel/trace v1.11.2
)

Expand All @@ -15,5 +16,6 @@ require (
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions instrumentation/github.com/gorilla/mux/otelmux/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0=
go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI=
go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU=
go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU=
go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0=
go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
38 changes: 23 additions & 15 deletions instrumentation/github.com/gorilla/mux/otelmux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,26 @@ func Middleware(service string, opts ...Option) mux.MiddlewareFunc {
if cfg.Propagators == nil {
cfg.Propagators = otel.GetTextMapPropagator()
}
if cfg.spanNameFormatter == nil {
cfg.spanNameFormatter = defaultSpanNameFunc
}
return func(handler http.Handler) http.Handler {
return traceware{
service: service,
tracer: tracer,
propagators: cfg.Propagators,
handler: handler,
service: service,
tracer: tracer,
propagators: cfg.Propagators,
handler: handler,
spanNameFormatter: cfg.spanNameFormatter,
}
}
}

type traceware struct {
service string
tracer oteltrace.Tracer
propagators propagation.TextMapPropagator
handler http.Handler
service string
tracer oteltrace.Tracer
propagators propagation.TextMapPropagator
handler http.Handler
spanNameFormatter func(string, *http.Request) string
}

type recordingResponseWriter struct {
Expand Down Expand Up @@ -111,32 +116,35 @@ func putRRW(rrw *recordingResponseWriter) {
rrwPool.Put(rrw)
}

// defaultSpanNameFunc just reuses the route name as the span name.
func defaultSpanNameFunc(routeName string, _ *http.Request) string { return routeName }

// ServeHTTP implements the http.Handler interface. It does the actual
// tracing of the request.
func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := tw.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanName := ""
routeStr := ""
route := mux.CurrentRoute(r)
if route != nil {
var err error
spanName, err = route.GetPathTemplate()
routeStr, err = route.GetPathTemplate()
if err != nil {
spanName, err = route.GetPathRegexp()
routeStr, err = route.GetPathRegexp()
if err != nil {
spanName = ""
routeStr = ""
}
}
}
routeStr := spanName
if spanName == "" {
spanName = fmt.Sprintf("HTTP %s route not found", r.Method)
if routeStr == "" {
routeStr = fmt.Sprintf("HTTP %s route not found", r.Method)
}
opts := []oteltrace.SpanStartOption{
oteltrace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", r)...),
oteltrace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(r)...),
oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(tw.service, routeStr, r)...),
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
}
spanName := tw.spanNameFormatter(routeStr, r)
ctx, span := tw.tracer.Start(ctx, spanName, opts...)
defer span.End()
r2 := r.WithContext(ctx)
Expand Down
48 changes: 48 additions & 0 deletions instrumentation/github.com/gorilla/mux/otelmux/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package otelmux
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
Expand All @@ -25,9 +26,12 @@ import (

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
)

Expand Down Expand Up @@ -162,3 +166,47 @@ func TestResponseWriterInterfaces(t *testing.T) {

router.ServeHTTP(w, r)
}

func TestCustomSpanNameFormatter(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()

tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))

routeTpl := "/user/{id}"

testdata := []struct {
spanNameFormatter func(string, *http.Request) string
expected string
}{
{nil, routeTpl},
{
func(string, *http.Request) string { return "custom" },
"custom",
},
{
func(name string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, name)
},
"GET " + routeTpl,
},
}

for i, d := range testdata {
t.Run(fmt.Sprintf("%d_%s", i, d.expected), func(t *testing.T) {
router := mux.NewRouter()
router.Use(Middleware("foobar", WithTracerProvider(tp), WithSpanNameFormatter(d.spanNameFormatter)))
router.HandleFunc(routeTpl, func(w http.ResponseWriter, r *http.Request) {})

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, r)

spans := exporter.GetSpans()
require.Len(t, spans, 1)
assert.Equal(t, d.expected, spans[0].Name)

exporter.Reset()
})
}
}