From e14116f403417cab265ee0fe9dbe0a428fef6c65 Mon Sep 17 00:00:00 2001 From: Alan Chester Date: Fri, 21 Nov 2025 19:32:22 -0500 Subject: [PATCH 1/2] Add http.route and server.port to OTLP metrics Fixes #12247 This change adds missing OpenTelemetry semantic convention attributes to HTTP server metrics: - http.route: The matched route pattern from the router - server.port: The server port extracted from the Host header The router middleware now stores the route pattern in the request context, making it available to the semantic convention metrics middleware for inclusion in metrics. --- .../content/observability/metrics/overview.md | 9 +++--- .../observability/observability.go | 16 ++++++++++- pkg/middlewares/observability/router.go | 8 ++++++ pkg/middlewares/observability/semconv.go | 13 +++++++++ pkg/middlewares/observability/semconv_test.go | 28 ++++++++++++++++++- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/docs/content/observability/metrics/overview.md b/docs/content/observability/metrics/overview.md index c4b57a3d2..8d66cf3ea 100644 --- a/docs/content/observability/metrics/overview.md +++ b/docs/content/observability/metrics/overview.md @@ -93,13 +93,13 @@ Here is a comprehensive list of labels that are provided by the global metrics: ## OpenTelemetry Semantic Conventions -Traefik Proxy follows [official OpenTelemetry semantic conventions v1.23.1](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/http/http-metrics.md). +Traefik Proxy follows [official OpenTelemetry semantic conventions v1.37.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.37.0/docs/http/http-metrics.md). ### HTTP Server -| Metric | Type | [Labels](#labels) | Description | -|-------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------| -| http.server.request.duration | Histogram | `error.type`, `http.request.method`, `http.response.status_code`, `network.protocol.name`, `server.address`, `server.port`, `url.scheme` | Duration of HTTP server requests | +| Metric | Type | [Labels](#labels) | Description | +|-------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------| +| http.server.request.duration | Histogram | `error.type`, `http.request.method`, `http.response.status_code`, `http.route`, `network.protocol.name`, `server.address`, `server.port`, `url.scheme` | Duration of HTTP server requests | #### Labels @@ -110,6 +110,7 @@ Here is a comprehensive list of labels that are provided by the metrics: | `error.type` | Describes a class of error the operation ended with | "500" | | `http.request.method` | HTTP request method | "GET" | | `http.response.status_code` | HTTP response status code | "200" | +| `http.route` | The matched route pattern (when available) | "/api/users" | | `network.protocol.name` | OSI application layer or non-OSI equivalent | "http/1.1" | | `network.protocol.version` | Version of the protocol specified in `network.protocol.name` | "1.1" | | `server.address` | Name of the local HTTP server that received the request | "example.com" | diff --git a/pkg/middlewares/observability/observability.go b/pkg/middlewares/observability/observability.go index a02ca292d..945bca269 100644 --- a/pkg/middlewares/observability/observability.go +++ b/pkg/middlewares/observability/observability.go @@ -11,7 +11,10 @@ import ( type contextKey int -const observabilityKey contextKey = iota +const ( + observabilityKey contextKey = iota + httpRouteKey +) type Observability struct { AccessLogsEnabled bool @@ -71,6 +74,17 @@ func SetStatusErrorf(ctx context.Context, format string, args ...any) { } } +// WithHTTPRoute injects the HTTP route pattern into the context. +func WithHTTPRoute(ctx context.Context, route string) context.Context { + return context.WithValue(ctx, httpRouteKey, route) +} + +// HTTPRoute returns the HTTP route pattern from the context, if available. +func HTTPRoute(ctx context.Context) (string, bool) { + route, ok := ctx.Value(httpRouteKey).(string) + return route, ok +} + func Proto(proto string) string { switch proto { case "HTTP/1.0": diff --git a/pkg/middlewares/observability/router.go b/pkg/middlewares/observability/router.go index e840a1aa4..82f0966bf 100644 --- a/pkg/middlewares/observability/router.go +++ b/pkg/middlewares/observability/router.go @@ -45,6 +45,14 @@ func newRouter(ctx context.Context, router, routerRule, service string, next htt } func (f *routerTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + // Store the HTTP route pattern in context for semantic convention metrics. + if f.routerRule != "" { + ctx = WithHTTPRoute(ctx, f.routerRule) + req = req.WithContext(ctx) + } + if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) { tracingCtx, span := tracer.Start(req.Context(), "Router", trace.WithSpanKind(trace.SpanKindInternal)) defer span.End() diff --git a/pkg/middlewares/observability/semconv.go b/pkg/middlewares/observability/semconv.go index 41aa52696..660406117 100644 --- a/pkg/middlewares/observability/semconv.go +++ b/pkg/middlewares/observability/semconv.go @@ -3,6 +3,7 @@ package observability import ( "context" "fmt" + "net" "net/http" "strconv" "strings" @@ -76,6 +77,18 @@ func (e *semConvServerMetrics) ServeHTTP(rw http.ResponseWriter, req *http.Reque attrs = append(attrs, semconv.NetworkProtocolVersion(Proto(req.Proto))) attrs = append(attrs, semconv.ServerAddress(req.Host)) + // Add http.route attribute if available from router context. + if route, ok := HTTPRoute(ctx); ok && route != "" { + attrs = append(attrs, semconv.HTTPRoute(route)) + } + + // Extract and add server.port attribute from req.Host. + if _, portStr, err := net.SplitHostPort(req.Host); err == nil { + if port, err := strconv.Atoi(portStr); err == nil { + attrs = append(attrs, semconv.ServerPort(port)) + } + } + e.semConvMetricRegistry.HTTPServerRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), httpconv.RequestMethodAttr(req.Method), req.Header.Get("X-Forwarded-Proto"), attrs...) } diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go index 960d4b98a..220b3f1c4 100644 --- a/pkg/middlewares/observability/semconv_test.go +++ b/pkg/middlewares/observability/semconv_test.go @@ -21,11 +21,14 @@ func TestSemConvServerMetrics(t *testing.T) { tests := []struct { desc string statusCode int + host string + httpRoute string wantAttributes attribute.Set }{ { desc: "not found status", statusCode: http.StatusNotFound, + host: "www.test.com", wantAttributes: attribute.NewSet( attribute.Key("error.type").String("404"), attribute.Key("http.request.method").String("GET"), @@ -39,6 +42,7 @@ func TestSemConvServerMetrics(t *testing.T) { { desc: "created status", statusCode: http.StatusCreated, + host: "www.test.com", wantAttributes: attribute.NewSet( attribute.Key("http.request.method").String("GET"), attribute.Key("http.response.status_code").Int(201), @@ -48,6 +52,22 @@ func TestSemConvServerMetrics(t *testing.T) { attribute.Key("url.scheme").String("http"), ), }, + { + desc: "with http.route and server.port", + statusCode: http.StatusOK, + host: "example.com:443", + httpRoute: "/api/banking", + wantAttributes: attribute.NewSet( + attribute.Key("http.request.method").String("GET"), + attribute.Key("http.response.status_code").Int(200), + attribute.Key("http.route").String("/api/banking"), + attribute.Key("network.protocol.name").String("http/1.1"), + attribute.Key("network.protocol.version").String("1.1"), + attribute.Key("server.address").String("example.com:443"), + attribute.Key("server.port").Int(443), + attribute.Key("url.scheme").String("http"), + ), + }, } for _, test := range tests { @@ -68,12 +88,18 @@ func TestSemConvServerMetrics(t *testing.T) { require.NoError(t, err) require.NotNil(t, semConvMetricRegistry) - req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil) + req := httptest.NewRequest(http.MethodGet, "http://"+test.host+"/search?q=Opentelemetry", nil) rw := httptest.NewRecorder() + req.Host = test.host req.RemoteAddr = "10.0.0.1:1234" req.Header.Set("User-Agent", "entrypoint-test") req.Header.Set("X-Forwarded-Proto", "http") + // Inject http.route into context if provided. + if test.httpRoute != "" { + req = req.WithContext(WithHTTPRoute(req.Context(), test.httpRoute)) + } + next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(test.statusCode) }) From debdc36d7712c656df686fa848f4ecf27d3062e6 Mon Sep 17 00:00:00 2001 From: mmatur Date: Fri, 30 Jan 2026 16:57:50 +0100 Subject: [PATCH 2/2] review --- go.mod | 48 ++++---- go.sum | 114 +++++++++--------- .../observability/observability.go | 50 ++++++-- pkg/middlewares/observability/router.go | 9 +- pkg/middlewares/observability/semconv.go | 24 ++-- pkg/middlewares/observability/semconv_test.go | 29 +++-- pkg/server/middleware/observability.go | 8 ++ 7 files changed, 168 insertions(+), 114 deletions(-) diff --git a/go.mod b/go.mod index 5c0935e89..24a2a511f 100644 --- a/go.mod +++ b/go.mod @@ -80,23 +80,23 @@ require ( github.com/vulcand/oxy/v2 v2.0.3 github.com/vulcand/predicate v1.2.0 github.com/yuin/gopher-lua v1.1.1 - go.opentelemetry.io/collector/pdata v1.41.0 - go.opentelemetry.io/contrib/bridges/otellogrus v0.13.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 - go.opentelemetry.io/otel/log v0.14.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/sdk/log v0.14.0 - go.opentelemetry.io/otel/sdk/metric v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/collector/pdata v1.50.0 + go.opentelemetry.io/contrib/bridges/otellogrus v0.14.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 + go.opentelemetry.io/otel/log v0.15.0 + go.opentelemetry.io/otel/metric v1.39.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/log v0.15.0 + go.opentelemetry.io/otel/sdk/metric v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 golang.org/x/crypto v0.47.0 golang.org/x/mod v0.31.0 golang.org/x/net v0.49.0 @@ -245,7 +245,7 @@ require ( github.com/gophercloud/gophercloud v1.14.1 // indirect github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/cronexpr v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -378,13 +378,13 @@ require ( go.mongodb.org/mongo-driver v1.13.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/featuregate v1.41.0 // indirect + go.opentelemetry.io/collector/featuregate v1.50.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect diff --git a/go.sum b/go.sum index 00b764cce..f11967440 100644 --- a/go.sum +++ b/go.sum @@ -650,8 +650,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.26.1 h1:5oSXOO5fboPZeW5SN+TdGFP/BILDgBm19OrPZ/pICIM= @@ -1380,67 +1380,69 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/featuregate v1.41.0 h1:CL4UMsMQj35nMJC3/jUu8VvYB4MHirbAX4B0Z/fCVLY= -go.opentelemetry.io/collector/featuregate v1.41.0/go.mod h1:A72x92glpH3zxekaUybml1vMSv94BH6jQRn5+/htcjw= -go.opentelemetry.io/collector/pdata v1.41.0 h1:2zurAaY0FkURbLa1x7f7ag6HaNZYZKSmI4wgzDegLgo= -go.opentelemetry.io/collector/pdata v1.41.0/go.mod h1:h0OghaTYe4oRvLxK31Ny7gkyjJ1p8oniM5MiCzluQjc= -go.opentelemetry.io/contrib/bridges/otellogrus v0.13.0 h1:Nzvgkys5xSchtkWEeTQNixr9EVo+cbYCpSey2zMftXw= -go.opentelemetry.io/contrib/bridges/otellogrus v0.13.0/go.mod h1:nvmPavMmeFjktIIxQAsE265cQ9nQ5qhDV2mN5kfdPog= +go.opentelemetry.io/collector/featuregate v1.50.0 h1:nROGw8VpLuc2/PExnL6ammUpr2y7pozpbwgae6zU4s0= +go.opentelemetry.io/collector/featuregate v1.50.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo= +go.opentelemetry.io/collector/internal/testutil v0.144.0 h1:lSI9FBQI21eAxJ/L52pAYxsvKhU5dm9HqXGnKp8XAes= +go.opentelemetry.io/collector/internal/testutil v0.144.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c= +go.opentelemetry.io/collector/pdata v1.50.0 h1:vES5c9jT9HzOhHEg1OIjPxk4qKIjA+Dao8dxU3oePU0= +go.opentelemetry.io/collector/pdata v1.50.0/go.mod h1:G18lFpQYh4473PiEPqLd7BKfc8a/j+Fl4EfHWy1Ylx8= +go.opentelemetry.io/contrib/bridges/otellogrus v0.14.0 h1:UtI97OoeD9Cjx/s1nQ4W9fCFjJbPfhTsVBorhCM2lQg= +go.opentelemetry.io/contrib/bridges/otellogrus v0.14.0/go.mod h1:L38Uc5BbIN4o6QKrxc252Le7FyE7Ym8IV9GMa9dr3I0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw= -go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg= -go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts= -go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= -go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc= -go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo= -go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr5sRNeQFMsN9Zn0EoI= -go.opentelemetry.io/contrib/propagators/ot v1.38.0/go.mod h1:2hDsuiHRO39SRUMhYGqmj64z/IuMRoxE4bBSFR82Lo8= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= -go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= -go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA= +go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE= +go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo= +go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY= +go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo= +go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8= +go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg= +go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= +go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= +go.opentelemetry.io/otel/log/logtest v0.15.0 h1:porNFuxAjodl6LhePevOc3n7bo3Wi3JhGXNWe7KP8iU= +go.opentelemetry.io/otel/log/logtest v0.15.0/go.mod h1:c8epqBXGHgS1LiNgmD+LuNYK9lSS3mqvtMdxLsfJgLg= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= +go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -go.opentelemetry.io/proto/slim/otlp v1.7.1 h1:lZ11gEokjIWYM3JWOUrIILr2wcf6RX+rq5SPObV9oyc= -go.opentelemetry.io/proto/slim/otlp v1.7.1/go.mod h1:uZ6LJWa49eNM/EXnnvJGTTu8miokU8RQdnO980LJ57g= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.0.1 h1:Tr/eXq6N7ZFjN+THBF/BtGLUz8dciA7cuzGRsCEkZ88= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.0.1/go.mod h1:riqUmAOJFDFuIAzZu/3V6cOrTyfWzpgNJnG5UwrapCk= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.0.1 h1:z/oMlrCv3Kopwh/dtdRagJy+qsRRPA86/Ux3g7+zFXM= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.0.1/go.mod h1:C7EHYSIiaALi9RnNORCVaPCQDuJgJEn/XxkctaTez1E= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= +go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/pkg/middlewares/observability/observability.go b/pkg/middlewares/observability/observability.go index 945bca269..be1fc00d4 100644 --- a/pkg/middlewares/observability/observability.go +++ b/pkg/middlewares/observability/observability.go @@ -13,9 +13,46 @@ type contextKey int const ( observabilityKey contextKey = iota - httpRouteKey + routeInfoKey ) +// RouteInfo holds routing information that can be set by the router +// and read by observability middleware after the request completes. +// This uses a pointer-based pattern (similar to capture middleware) +// to allow the router to set values that are visible to middleware +// running at the entrypoint level. +type RouteInfo struct { + route string +} + +// SetRoute sets the HTTP route pattern. +func (r *RouteInfo) SetRoute(route string) { + if r != nil { + r.route = route + } +} + +// Route returns the HTTP route pattern. +func (r *RouteInfo) Route() string { + if r == nil { + return "" + } + return r.route +} + +// WithRouteInfo injects a RouteInfo pointer into the context. +// This should be called early in the middleware chain. +func WithRouteInfo(ctx context.Context) context.Context { + return context.WithValue(ctx, routeInfoKey, &RouteInfo{}) +} + +// RouteInfoFromContext returns the RouteInfo from the context. +// Returns nil if no RouteInfo was set. +func RouteInfoFromContext(ctx context.Context) *RouteInfo { + ri, _ := ctx.Value(routeInfoKey).(*RouteInfo) + return ri +} + type Observability struct { AccessLogsEnabled bool MetricsEnabled bool @@ -74,17 +111,6 @@ func SetStatusErrorf(ctx context.Context, format string, args ...any) { } } -// WithHTTPRoute injects the HTTP route pattern into the context. -func WithHTTPRoute(ctx context.Context, route string) context.Context { - return context.WithValue(ctx, httpRouteKey, route) -} - -// HTTPRoute returns the HTTP route pattern from the context, if available. -func HTTPRoute(ctx context.Context) (string, bool) { - route, ok := ctx.Value(httpRouteKey).(string) - return route, ok -} - func Proto(proto string) string { switch proto { case "HTTP/1.0": diff --git a/pkg/middlewares/observability/router.go b/pkg/middlewares/observability/router.go index 82f0966bf..b30233d5f 100644 --- a/pkg/middlewares/observability/router.go +++ b/pkg/middlewares/observability/router.go @@ -47,13 +47,14 @@ func newRouter(ctx context.Context, router, routerRule, service string, next htt func (f *routerTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() - // Store the HTTP route pattern in context for semantic convention metrics. + // Store the HTTP route pattern for semantic convention metrics. if f.routerRule != "" { - ctx = WithHTTPRoute(ctx, f.routerRule) - req = req.WithContext(ctx) + if ri := RouteInfoFromContext(ctx); ri != nil { + ri.SetRoute(f.routerRule) + } } - if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) { + if tracer := tracing.TracerFromContext(ctx); tracer != nil && DetailedTracingEnabled(ctx) { tracingCtx, span := tracer.Start(req.Context(), "Router", trace.WithSpanKind(trace.SpanKindInternal)) defer span.End() diff --git a/pkg/middlewares/observability/semconv.go b/pkg/middlewares/observability/semconv.go index 660406117..920570806 100644 --- a/pkg/middlewares/observability/semconv.go +++ b/pkg/middlewares/observability/semconv.go @@ -38,7 +38,7 @@ func SemConvServerMetricsHandler(ctx context.Context, semConvMetricRegistry *met // newServerMetricsSemConv creates a new semConv server metrics middleware for incoming requests. func newServerMetricsSemConv(ctx context.Context, semConvMetricRegistry *metrics.SemConvMetricsRegistry, next http.Handler) http.Handler { - middlewares.GetLogger(ctx, "tracing", semConvServerMetricsTypeName).Debug().Msg("Creating middleware") + middlewares.GetLogger(ctx, "metrics", semConvServerMetricsTypeName).Debug().Msg("Creating middleware") return &semConvServerMetrics{ semConvMetricRegistry: semConvMetricRegistry, @@ -75,18 +75,26 @@ func (e *semConvServerMetrics) ServeHTTP(rw http.ResponseWriter, req *http.Reque attrs = append(attrs, semconv.HTTPResponseStatusCode(capt.StatusCode())) attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto))) attrs = append(attrs, semconv.NetworkProtocolVersion(Proto(req.Proto))) - attrs = append(attrs, semconv.ServerAddress(req.Host)) // Add http.route attribute if available from router context. - if route, ok := HTTPRoute(ctx); ok && route != "" { - attrs = append(attrs, semconv.HTTPRoute(route)) + if ri := RouteInfoFromContext(ctx); ri != nil && ri.Route() != "" { + attrs = append(attrs, semconv.HTTPRoute(ri.Route())) } - // Extract and add server.port attribute from req.Host. - if _, portStr, err := net.SplitHostPort(req.Host); err == nil { - if port, err := strconv.Atoi(portStr); err == nil { - attrs = append(attrs, semconv.ServerPort(port)) + host, port, err := net.SplitHostPort(req.URL.Host) + if err != nil { + attrs = append(attrs, semconv.ServerAddress(req.Host)) + // No port in URL.Host - use default port based on scheme. + switch req.URL.Scheme { + case "http": + attrs = append(attrs, semconv.ServerPort(80)) + case "https": + attrs = append(attrs, semconv.ServerPort(443)) } + } else { + intPort, _ := strconv.Atoi(port) + attrs = append(attrs, semconv.ServerAddress(host)) + attrs = append(attrs, semconv.ServerPort(intPort)) } e.semConvMetricRegistry.HTTPServerRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go index 220b3f1c4..9615d6cd1 100644 --- a/pkg/middlewares/observability/semconv_test.go +++ b/pkg/middlewares/observability/semconv_test.go @@ -36,6 +36,7 @@ func TestSemConvServerMetrics(t *testing.T) { attribute.Key("network.protocol.name").String("http/1.1"), attribute.Key("network.protocol.version").String("1.1"), attribute.Key("server.address").String("www.test.com"), + attribute.Key("server.port").Int(80), attribute.Key("url.scheme").String("http"), ), }, @@ -49,6 +50,7 @@ func TestSemConvServerMetrics(t *testing.T) { attribute.Key("network.protocol.name").String("http/1.1"), attribute.Key("network.protocol.version").String("1.1"), attribute.Key("server.address").String("www.test.com"), + attribute.Key("server.port").Int(80), attribute.Key("url.scheme").String("http"), ), }, @@ -63,7 +65,7 @@ func TestSemConvServerMetrics(t *testing.T) { attribute.Key("http.route").String("/api/banking"), attribute.Key("network.protocol.name").String("http/1.1"), attribute.Key("network.protocol.version").String("1.1"), - attribute.Key("server.address").String("example.com:443"), + attribute.Key("server.address").String("example.com"), attribute.Key("server.port").Int(443), attribute.Key("url.scheme").String("http"), ), @@ -95,22 +97,29 @@ func TestSemConvServerMetrics(t *testing.T) { req.Header.Set("User-Agent", "entrypoint-test") req.Header.Set("X-Forwarded-Proto", "http") - // Inject http.route into context if provided. - if test.httpRoute != "" { - req = req.WithContext(WithHTTPRoute(req.Context(), test.httpRoute)) - } - - next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + httpRoute := test.httpRoute + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Simulate the router setting the route via RouteInfo. + if httpRoute != "" { + if ri := RouteInfoFromContext(req.Context()); ri != nil { + ri.SetRoute(httpRoute) + } + } rw.WriteHeader(test.statusCode) }) - handler := newServerMetricsSemConv(t.Context(), semConvMetricRegistry, next) + semConvHandler := newServerMetricsSemConv(t.Context(), semConvMetricRegistry, next) - handler, err = capture.Wrap(handler) + captureHandler, err := capture.Wrap(semConvHandler) require.NoError(t, err) + // Inject RouteInfo pointer so the simulated router can set the route. + routeInfoHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + captureHandler.ServeHTTP(rw, req.WithContext(WithRouteInfo(req.Context()))) + }) + // Injection of the observability variables in the request context. - handler = WithObservabilityHandler(handler, Observability{ + handler := WithObservabilityHandler(routeInfoHandler, Observability{ SemConvMetricsEnabled: true, }) diff --git a/pkg/server/middleware/observability.go b/pkg/server/middleware/observability.go index 0ffe34165..685c86783 100644 --- a/pkg/server/middleware/observability.go +++ b/pkg/server/middleware/observability.go @@ -55,6 +55,14 @@ func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName stri return o.observabilityContextHandler(next, internal, config), nil }) + // Inject RouteInfo pointer early so the router can set the route + // and semconv metrics can read it after the request completes. + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + next.ServeHTTP(rw, req.WithContext(observability.WithRouteInfo(req.Context()))) + }), nil + }) + // Capture middleware for accessLogs or metrics. if o.shouldAccessLog(internal, config) || o.shouldMeter(internal, config) || o.shouldMeterSemConv(internal, config) { chain = chain.Append(capture.Wrap)