From 17cbf3f60ce84ae5f5e60b17b1bc61826d2b374e Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Mon, 16 Feb 2026 15:07:28 -0800 Subject: [PATCH] otel: set http.route span attribute using otel SDK directly Replace the removed otelhttp.WithRouteTag with a direct call to trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute(...)) in each generated handler. This restores the http.route span attribute that was lost when WithRouteTag was removed in otelhttp v0.65.0. The plugin is needed when otelhttp wraps the mux externally via otelhttp.NewHandler(mux, ...), since r.Pattern is not available before the mux dispatches. When using mux.Use(otelhttp.NewMiddleware(...)) instead, the Goa muxer sets r.Pattern early and the plugin is not necessary. --- otel/README.md | 78 ++++++++++++++++++---------- otel/generate.go | 58 ++++++++++++++++----- otel/generate_test.go | 4 -- otel/testdata/multiple routes.golden | 10 +++- otel/testdata/one route.golden | 5 +- 5 files changed, 107 insertions(+), 48 deletions(-) diff --git a/otel/README.md b/otel/README.md index 394329288..e745ac4d0 100644 --- a/otel/README.md +++ b/otel/README.md @@ -1,28 +1,50 @@ -# OpenTelemetry Plugin (Deprecated) - -> **Deprecated**: This plugin is no longer necessary and will be removed in a -> future release. The Goa HTTP muxer now sets `r.Pattern` on every matched -> request (using the Go 1.22+ convention), which `otelhttp` v0.65.0+ reads -> automatically to tag spans and metrics with the matched route. -> -> **Migration**: Remove the blank import from your design package and -> regenerate: -> -> ```diff -> - import _ "goa.design/plugins/v3/otel" -> ``` -> -> No other changes are needed — route tagging now happens automatically in -> the Goa muxer. - -## Background - -The `otel` plugin was a [Goa](https://github.com/goadesign/goa/tree/v3) plugin -that wrapped generated HTTP handlers with `otelhttp.WithRouteTag` to set the -`http.route` attribute on OpenTelemetry spans and metrics. - -`otelhttp.WithRouteTag` was -[removed](https://github.com/open-telemetry/opentelemetry-go-contrib/pull/8268) -in `otelhttp` v0.65.0 because `otelhttp` now reads `r.Pattern` (added in Go -1.22) to obtain the route automatically. Goa's muxer has been updated to set -`r.Pattern` on every dispatched request, making this plugin unnecessary. +# OpenTelemetry Plugin + +The `otel` plugin is a [Goa](https://github.com/goadesign/goa/tree/v3) plugin +that sets the `http.route` OpenTelemetry span attribute on generated HTTP +handlers. This ensures the matched route pattern appears as a span attribute +regardless of how `otelhttp` is wired into the application. + +## Usage + +Import the plugin in the service design package with a blank identifier: + +```go +package design + +import . "goa.design/goa/v3/dsl" +import _ "goa.design/plugins/v3/otel" + +var _ = API("... +``` + +and generate as usual: + +```bash +goa gen PACKAGE +``` + +The generated `MountXxxHandler` functions will set `http.route` on the active +span before calling the handler: + +```go +mux.Handle("GET", "/users/{id}", func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/users/{id}")) + f(w, r) +}) +``` + +## Alternative: mux middleware (no plugin needed) + +As of Goa v3.25.0 the default muxer sets `r.Pattern` before middlewares run. +If you register `otelhttp` as a mux middleware, it reads the route pattern +automatically and this plugin is not necessary: + +```go +mux := goahttp.NewMuxer() +mux.Use(otelhttp.NewMiddleware("service")) +``` + +Use the plugin when you wrap the mux externally with +`otelhttp.NewHandler(mux, ...)`, since in that case `r.Pattern` is not yet +available when `otelhttp` creates the span. diff --git a/otel/generate.go b/otel/generate.go index 3a7c4d630..b286d16ad 100644 --- a/otel/generate.go +++ b/otel/generate.go @@ -1,16 +1,24 @@ -// Package otel was a Goa plugin that instrumented HTTP handlers with -// otelhttp.WithRouteTag to set the http.route attribute on spans and metrics. +// Package otel is a Goa plugin that sets the http.route OpenTelemetry span +// attribute on generated HTTP handlers. It does this by wrapping each handler +// to call trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute(...)) +// before the handler executes. // -// Deprecated: As of Goa v3.x.x the default HTTP muxer sets r.Pattern on every -// matched request, which otelhttp (v0.65.0+) reads automatically to tag spans -// and metrics with the matched route. This plugin is no longer necessary and -// will be removed in a future release. Remove the blank import from your -// design package: +// Import the package in the service design with a blank identifier: // -// import _ "goa.design/plugins/v3/otel" // ← delete this line +// import _ "goa.design/plugins/v3/otel" +// +// When using otelhttp as a mux middleware (recommended), the Goa muxer sets +// r.Pattern before middlewares run, so otelhttp picks up the route +// automatically. In that case this plugin is not necessary: +// +// mux := goahttp.NewMuxer() +// mux.Use(otelhttp.NewMiddleware("service")) package otel import ( + "path/filepath" + "strings" + "goa.design/goa/v3/codegen" "goa.design/goa/v3/eval" ) @@ -20,10 +28,34 @@ func init() { codegen.RegisterPluginLast("otel", "gen", nil, Generate) } -// Generate is a no-op kept for backward compatibility. The Goa HTTP muxer now -// sets r.Pattern on every request, making explicit route tagging unnecessary. -// -// Deprecated: Remove the otel plugin import from your design package. -func Generate(_ string, _ []eval.Root, files []*codegen.File) ([]*codegen.File, error) { +// Generate modifies the generated HTTP server code to set the http.route +// OpenTelemetry span attribute on each handler. This ensures the attribute +// is present regardless of how otelhttp is wired (external handler wrapping +// or mux middleware). +func Generate(genpkg string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) { + for _, f := range files { + if filepath.Base(f.Path) != "server.go" { + continue + } + for _, s := range f.SectionTemplates { + if s.Name == "server-handler" { + s.Source = strings.Replace( + s.Source, + `mux.Handle("{{ .Verb }}", "{{ .Path }}", f)`, + `mux.Handle("{{ .Verb }}", "{{ .Path }}", func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("{{ .Path }}")) + f(w, r) + })`, + 1, + ) + } + } + imports := f.SectionTemplates[0].Data.(map[string]any)["Imports"].([]*codegen.ImportSpec) + imports = append(imports, + &codegen.ImportSpec{Path: "go.opentelemetry.io/otel/trace"}, + &codegen.ImportSpec{Path: "go.opentelemetry.io/otel/semconv/v1.38.0", Name: "semconv"}, + ) + f.SectionTemplates[0].Data.(map[string]any)["Imports"] = imports + } return files, nil } diff --git a/otel/generate_test.go b/otel/generate_test.go index d79e3e86e..f2e9142d1 100644 --- a/otel/generate_test.go +++ b/otel/generate_test.go @@ -37,10 +37,6 @@ func TestOtel(t *testing.T) { fs, err := Generate("", []eval.Root{root}, serverFiles) assert.NoError(t, err) require.Len(t, fs, 2) - // Generate is a no-op: the returned files must be identical - // to the input server files. - assert.Same(t, serverFiles[0], fs[0]) - assert.Same(t, serverFiles[1], fs[1]) sections := fs[0].Section("server-handler") require.Len(t, sections, 1) section := sections[0] diff --git a/otel/testdata/multiple routes.golden b/otel/testdata/multiple routes.golden index 7605fbd68..0f2b75507 100644 --- a/otel/testdata/multiple routes.golden +++ b/otel/testdata/multiple routes.golden @@ -7,6 +7,12 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) { h.ServeHTTP(w, r) } } - mux.Handle("GET", "/", f) - mux.Handle("GET", "/other", f) + mux.Handle("GET", "/", func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/")) + f(w, r) + }) + mux.Handle("GET", "/other", func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/other")) + f(w, r) + }) } diff --git a/otel/testdata/one route.golden b/otel/testdata/one route.golden index 827900a3b..f8b34d226 100644 --- a/otel/testdata/one route.golden +++ b/otel/testdata/one route.golden @@ -7,5 +7,8 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) { h.ServeHTTP(w, r) } } - mux.Handle("GET", "/", f) + mux.Handle("GET", "/", func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/")) + f(w, r) + }) }