diff --git a/otel/README.md b/otel/README.md index 39432928..e745ac4d 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 3a7c4d63..b286d16a 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 d79e3e86..f2e9142d 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 7605fbd6..0f2b7550 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 827900a3..f8b34d22 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) + }) }