Skip to content
Draft
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 @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Use Gin's own `ClientIP` method to detect the client's IP, which supports custom proxy headers in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#6095)
- Added test for Fields in `go.opentelemetry.io/contrib/propagators/jaeger`. (#7119)
- Allow configuring samplers in `go.opentelemetry.io/contrib/otelconf`. (#7148)
- Rerun the span name formatter after the request ran if a `req.Pattern` is set, so the span name can include it in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#7192)

### Changed

Expand Down
30 changes: 28 additions & 2 deletions instrumentation/net/http/otelhttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

import (
"cmp"
"context"
"net/http"
"net/http/httptrace"
Expand Down Expand Up @@ -174,9 +175,13 @@ func WithMessageEvents(events ...event) Option {
})
}

// WithSpanNameFormatter takes a function that will be called on every
// WithSpanNameFormatter takes a [SpanNameFormatter] function that will be called on every
// request and the returned string will become the Span Name.
func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Option {
//
// When using `http.ServeMux` (or any middleware that sets `request.Pattern`),
// the span name formatter will run twice. Once when the span is created, and
// once after the middleware, so the pattern can be used.
func WithSpanNameFormatter(f SpanNameFormatter) Option {
return optionFunc(func(c *config) {
c.SpanNameFormatter = f
})
Expand Down Expand Up @@ -205,3 +210,24 @@ func WithMetricAttributesFn(metricAttributesFn func(r *http.Request) []attribute
c.MetricAttributesFn = metricAttributesFn
})
}

// SpanNameFormatter returns the span name to use for a given request.
type SpanNameFormatter = func(operation string, r *http.Request) string

// SpanNameFromOperation always uses the operation name as the span name.
// It is the default formatter for handlers.
func SpanNameFromOperation(operation string, _ *http.Request) string {
return operation
}

// SpanNameFromPattern uses the matched request pattern as the span name.
// It falls back to the operation name if there is no pattern.
func SpanNameFromPattern(operation string, r *http.Request) string {
return cmp.Or(r.Pattern, operation)
}

// SpanNameFromMethod uses "HTTP " + the request method as the span name.
// It is the default formatter for transports.
func SpanNameFromMethod(_ string, r *http.Request) string {
return "HTTP " + r.Method
}
29 changes: 16 additions & 13 deletions instrumentation/net/http/otelhttp/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,32 +60,33 @@ func TestBasicFilter(t *testing.T) {
func TestSpanNameFormatter(t *testing.T) {
testCases := []struct {
name string
formatter func(s string, r *http.Request) string
formatter otelhttp.SpanNameFormatter
operation string
expected string
}{
{
name: "default handler formatter",
formatter: func(operation string, _ *http.Request) string {
return operation
},
name: "default handler formatter",
formatter: otelhttp.SpanNameFromOperation,
operation: "test_operation",
expected: "test_operation",
},
{
name: "default transport formatter",
formatter: func(_ string, r *http.Request) string {
return "HTTP " + r.Method
},
expected: "HTTP GET",
name: "default transport formatter",
formatter: otelhttp.SpanNameFromMethod,
expected: "HTTP GET",
},
{
name: "request pattern formatter",
formatter: otelhttp.SpanNameFromPattern,
expected: "GET /hello/{thing}",
},
{
name: "custom formatter",
formatter: func(s string, r *http.Request) string {
return r.URL.Path
},
operation: "",
expected: "/hello",
expected: "/hello/world",
},
}

Expand All @@ -100,13 +101,15 @@ func TestSpanNameFormatter(t *testing.T) {
t.Fatal(err)
}
})
mux := http.NewServeMux()
mux.Handle("GET /hello/{thing}", handler)
h := otelhttp.NewHandler(
handler,
mux,
tc.operation,
otelhttp.WithTracerProvider(provider),
otelhttp.WithSpanNameFormatter(tc.formatter),
)
r, err := http.NewRequest(http.MethodGet, "http://localhost/hello", nil)
r, err := http.NewRequest(http.MethodGet, "http://localhost/hello/world", nil)
if err != nil {
t.Fatal(err)
}
Expand Down
15 changes: 8 additions & 7 deletions instrumentation/net/http/otelhttp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,14 @@ type middleware struct {
readEvent bool
writeEvent bool
filters []Filter
spanNameFormatter func(string, *http.Request) string
spanNameFormatter SpanNameFormatter
publicEndpoint bool
publicEndpointFn func(*http.Request) bool
metricAttributesFn func(*http.Request) []attribute.KeyValue

semconv semconv.HTTPServer
}

func defaultHandlerFormatter(operation string, _ *http.Request) string {
return operation
}

// NewHandler wraps the passed handler in a span named after the operation and
// enriches it with metrics.
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
Expand All @@ -56,7 +52,7 @@ func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Han

defaultOpts := []Option{
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
WithSpanNameFormatter(defaultHandlerFormatter),
WithSpanNameFormatter(SpanNameFromOperation),
}

c := newConfig(append(defaultOpts, opts...)...)
Expand Down Expand Up @@ -176,7 +172,12 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
ctx = ContextWithLabeler(ctx, labeler)
}

next.ServeHTTP(w, r.WithContext(ctx))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)

if r.Pattern != "" {
span.SetName(h.spanNameFormatter(h.operation, r))
}

statusCode := rww.StatusCode()
bytesWritten := rww.BytesWritten()
Expand Down
60 changes: 60 additions & 0 deletions instrumentation/net/http/otelhttp/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,66 @@ func TestHandlerRequestWithTraceContext(t *testing.T) {
assert.Equal(t, spans[1].SpanContext().SpanID(), spans[0].Parent().SpanID())
}

func TestWithSpanNameFormatter(t *testing.T) {
for _, tt := range []struct {
name string

formatter func(operation string, r *http.Request) string
wantSpanName string
}{
{
name: "with the default span name formatter",
wantSpanName: "test_handler",
},
{
name: "with a custom span name formatter",
formatter: func(op string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, r.URL.Path)
},
wantSpanName: "GET /foo/123",
},
{
name: "with a custom span name formatter using the pattern",
formatter: func(op string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, r.Pattern)
},
wantSpanName: "GET /foo/{id}",
},
} {
t.Run(tt.name, func(t *testing.T) {
spanRecorder := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(spanRecorder),
)

opts := []Option{
WithTracerProvider(provider),
}
if tt.formatter != nil {
opts = append(opts, WithSpanNameFormatter(tt.formatter))
}

mux := http.NewServeMux()
mux.Handle("/foo/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Nothing to do here
}))
h := NewHandler(mux, "test_handler", opts...)

r, err := http.NewRequest(http.MethodGet, "http://localhost/foo/123", nil)
require.NoError(t, err)

rr := httptest.NewRecorder()
h.ServeHTTP(rr, r)
assert.Equal(t, http.StatusOK, rr.Result().StatusCode)

assert.NoError(t, spanRecorder.ForceFlush(context.Background()))
spans := spanRecorder.Ended()
assert.Len(t, spans, 1)
assert.Equal(t, tt.wantSpanName, spans[0].Name())
})
}
}

func TestWithPublicEndpoint(t *testing.T) {
spanRecorder := tracetest.NewSpanRecorder()
provider := sdktrace.NewTracerProvider(
Expand Down
6 changes: 1 addition & 5 deletions instrumentation/net/http/otelhttp/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewTransport(base http.RoundTripper, opts ...Option) *Transport {

defaultOpts := []Option{
WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)),
WithSpanNameFormatter(defaultTransportFormatter),
WithSpanNameFormatter(SpanNameFromMethod),
}

c := newConfig(append(defaultOpts, opts...)...)
Expand All @@ -76,10 +76,6 @@ func (t *Transport) applyConfig(c *config) {
t.metricAttributesFn = c.MetricAttributesFn
}

func defaultTransportFormatter(_ string, r *http.Request) string {
return "HTTP " + r.Method
}

// RoundTrip creates a Span and propagates its context via the provided request's headers
// before handing the request to the configured base RoundTripper. The created span will
// end when the response body is closed or when a read from the body returns io.EOF.
Expand Down
Loading