Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
Automatic route/pattern naming for `http.ServeMux`
### Problem Statement

Go recently added more sophisticated pattern matching to `http.ServeMux`, including matching methods and path variables. I think this patterns can make good span names: They are descriptive, but also bounded (unlike request paths).

Furthermore, in Go 1.23, `http.ServeMux` [populates `http.Request.Pattern`](https://github.com/golang/go/issues/66405), making the pattern that matched a request available for handlers and, more importantly, middlewares. It would be great if `otelhttp` provided, out of the box, an option to automatically name spans from `http.ServeMux` patterns.

### Proposed Solution

A middleware can be used to name spans using the mux pattern that matched:

```go
// spanNameFromPattern is a simple middleware that sets the name of the span in the request context to the pattern used
// to match this request.
func spanNameFromPattern(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Call handler first, so http.ServeMux can populate r.Pattern
		next.ServeHTTP(w, r)
		// Set span name after the fact. As long as this middleware is used within otelhttp.Handler, the span should
		// still be open and thus renameable.
		trace.SpanFromContext(r.Context()).SetName(r.Pattern)
	})
}
```

I'm currently using this in this fashion:

```go
handler = otelhttp.NewHandler(
	// Instead of using a span name formatter, this middleware sets the span name using the mux pattern.
	spanNameFromPattern(handler),
	"http",
	// More options...
)
```

I think this could be a nice addition to the `otelhttp` package, so users don't need to write `spanNameFromPattern` on their own. We could fashion it as an option, which transparently adds this middleware:

```go
handler = otelhttp.NewHandler(
	handler,
	"http",
	otelhttp.WithSpanNameFromMuxPattern(), // Tentative name.
)
```

Or perhaps expose it as a middleware itself:

```go
handler = otelhttp.NewHandler(
	otelhttp.SpanNamer(handler), // VERY tentative name.
	"http",
)
```

Furthermore, I think some more sophistication could be added to the middleware, for example checking if the pattern already includes a method, and add it if it doesn't.

#### Alternatives

Have users write this middleware themselves, or set span names manually in application logic.

### Additional Context

I'd be happy to make a PR to `otelhttp` if you think this can be useful to have in there :)


Pull request:
otelhttp: handle request Pattern
Closes #6193
Handles otelhttp for #6919 (@Ananyasinha13 I know you're assigned this, but it has been 2 weeks with no movement).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e913c6bd0..0e4b1f60b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Add `http.route` metric attribute in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#7275)
 - Add the `WithSpanStartOptions` option to add custom options to new spans `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#7261)
 - Add instrumentation support for `go.mongodb.org/mongo-driver/v2` in `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo`. (#6539)
+- 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
 
diff --git a/instrumentation/net/http/otelhttp/config.go b/instrumentation/net/http/otelhttp/config.go
index a01bfafbe..6bd50d4c9 100644
--- a/instrumentation/net/http/otelhttp/config.go
+++ b/instrumentation/net/http/otelhttp/config.go
@@ -176,6 +176,10 @@ func WithMessageEvents(events ...event) Option {
 
 // WithSpanNameFormatter takes a function that will be called on every
 // request and the returned string will become the Span Name.
+//
+// When using [http.ServeMux] (or any middleware that sets the Pattern of [http.Request]),
+// the span name formatter will run twice. Once when the span is created, and
+// second time after the middleware, so the pattern can be used.
 func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Option {
 	return optionFunc(func(c *config) {
 		c.SpanNameFormatter = f
diff --git a/instrumentation/net/http/otelhttp/handler.go b/instrumentation/net/http/otelhttp/handler.go
index de36ec912..937f9b4e7 100644
--- a/instrumentation/net/http/otelhttp/handler.go
+++ b/instrumentation/net/http/otelhttp/handler.go
@@ -176,7 +176,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()


Test patch:
diff --git a/instrumentation/net/http/otelhttp/handler_test.go b/instrumentation/net/http/otelhttp/handler_test.go
index d05f76245..ee32914a6 100644
--- a/instrumentation/net/http/otelhttp/handler_test.go
+++ b/instrumentation/net/http/otelhttp/handler_test.go
@@ -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(