Skip to content

Commit a63ea42

Browse files
authored
otel: set http.route span attribute using otel SDK directly (#243)
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.
1 parent 688f48f commit a63ea42

5 files changed

Lines changed: 107 additions & 48 deletions

File tree

otel/README.md

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
1-
# OpenTelemetry Plugin (Deprecated)
2-
3-
> **Deprecated**: This plugin is no longer necessary and will be removed in a
4-
> future release. The Goa HTTP muxer now sets `r.Pattern` on every matched
5-
> request (using the Go 1.22+ convention), which `otelhttp` v0.65.0+ reads
6-
> automatically to tag spans and metrics with the matched route.
7-
>
8-
> **Migration**: Remove the blank import from your design package and
9-
> regenerate:
10-
>
11-
> ```diff
12-
> - import _ "goa.design/plugins/v3/otel"
13-
> ```
14-
>
15-
> No other changes are needed — route tagging now happens automatically in
16-
> the Goa muxer.
17-
18-
## Background
19-
20-
The `otel` plugin was a [Goa](https://github.com/goadesign/goa/tree/v3) plugin
21-
that wrapped generated HTTP handlers with `otelhttp.WithRouteTag` to set the
22-
`http.route` attribute on OpenTelemetry spans and metrics.
23-
24-
`otelhttp.WithRouteTag` was
25-
[removed](https://github.com/open-telemetry/opentelemetry-go-contrib/pull/8268)
26-
in `otelhttp` v0.65.0 because `otelhttp` now reads `r.Pattern` (added in Go
27-
1.22) to obtain the route automatically. Goa's muxer has been updated to set
28-
`r.Pattern` on every dispatched request, making this plugin unnecessary.
1+
# OpenTelemetry Plugin
2+
3+
The `otel` plugin is a [Goa](https://github.com/goadesign/goa/tree/v3) plugin
4+
that sets the `http.route` OpenTelemetry span attribute on generated HTTP
5+
handlers. This ensures the matched route pattern appears as a span attribute
6+
regardless of how `otelhttp` is wired into the application.
7+
8+
## Usage
9+
10+
Import the plugin in the service design package with a blank identifier:
11+
12+
```go
13+
package design
14+
15+
import . "goa.design/goa/v3/dsl"
16+
import _ "goa.design/plugins/v3/otel"
17+
18+
var _ = API("...
19+
```
20+
21+
and generate as usual:
22+
23+
```bash
24+
goa gen PACKAGE
25+
```
26+
27+
The generated `MountXxxHandler` functions will set `http.route` on the active
28+
span before calling the handler:
29+
30+
```go
31+
mux.Handle("GET", "/users/{id}", func(w http.ResponseWriter, r *http.Request) {
32+
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/users/{id}"))
33+
f(w, r)
34+
})
35+
```
36+
37+
## Alternative: mux middleware (no plugin needed)
38+
39+
As of Goa v3.25.0 the default muxer sets `r.Pattern` before middlewares run.
40+
If you register `otelhttp` as a mux middleware, it reads the route pattern
41+
automatically and this plugin is not necessary:
42+
43+
```go
44+
mux := goahttp.NewMuxer()
45+
mux.Use(otelhttp.NewMiddleware("service"))
46+
```
47+
48+
Use the plugin when you wrap the mux externally with
49+
`otelhttp.NewHandler(mux, ...)`, since in that case `r.Pattern` is not yet
50+
available when `otelhttp` creates the span.

otel/generate.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
// Package otel was a Goa plugin that instrumented HTTP handlers with
2-
// otelhttp.WithRouteTag to set the http.route attribute on spans and metrics.
1+
// Package otel is a Goa plugin that sets the http.route OpenTelemetry span
2+
// attribute on generated HTTP handlers. It does this by wrapping each handler
3+
// to call trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute(...))
4+
// before the handler executes.
35
//
4-
// Deprecated: As of Goa v3.x.x the default HTTP muxer sets r.Pattern on every
5-
// matched request, which otelhttp (v0.65.0+) reads automatically to tag spans
6-
// and metrics with the matched route. This plugin is no longer necessary and
7-
// will be removed in a future release. Remove the blank import from your
8-
// design package:
6+
// Import the package in the service design with a blank identifier:
97
//
10-
// import _ "goa.design/plugins/v3/otel" // ← delete this line
8+
// import _ "goa.design/plugins/v3/otel"
9+
//
10+
// When using otelhttp as a mux middleware (recommended), the Goa muxer sets
11+
// r.Pattern before middlewares run, so otelhttp picks up the route
12+
// automatically. In that case this plugin is not necessary:
13+
//
14+
// mux := goahttp.NewMuxer()
15+
// mux.Use(otelhttp.NewMiddleware("service"))
1116
package otel
1217

1318
import (
19+
"path/filepath"
20+
"strings"
21+
1422
"goa.design/goa/v3/codegen"
1523
"goa.design/goa/v3/eval"
1624
)
@@ -20,10 +28,34 @@ func init() {
2028
codegen.RegisterPluginLast("otel", "gen", nil, Generate)
2129
}
2230

23-
// Generate is a no-op kept for backward compatibility. The Goa HTTP muxer now
24-
// sets r.Pattern on every request, making explicit route tagging unnecessary.
25-
//
26-
// Deprecated: Remove the otel plugin import from your design package.
27-
func Generate(_ string, _ []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
31+
// Generate modifies the generated HTTP server code to set the http.route
32+
// OpenTelemetry span attribute on each handler. This ensures the attribute
33+
// is present regardless of how otelhttp is wired (external handler wrapping
34+
// or mux middleware).
35+
func Generate(genpkg string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
36+
for _, f := range files {
37+
if filepath.Base(f.Path) != "server.go" {
38+
continue
39+
}
40+
for _, s := range f.SectionTemplates {
41+
if s.Name == "server-handler" {
42+
s.Source = strings.Replace(
43+
s.Source,
44+
`mux.Handle("{{ .Verb }}", "{{ .Path }}", f)`,
45+
`mux.Handle("{{ .Verb }}", "{{ .Path }}", func(w http.ResponseWriter, r *http.Request) {
46+
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("{{ .Path }}"))
47+
f(w, r)
48+
})`,
49+
1,
50+
)
51+
}
52+
}
53+
imports := f.SectionTemplates[0].Data.(map[string]any)["Imports"].([]*codegen.ImportSpec)
54+
imports = append(imports,
55+
&codegen.ImportSpec{Path: "go.opentelemetry.io/otel/trace"},
56+
&codegen.ImportSpec{Path: "go.opentelemetry.io/otel/semconv/v1.38.0", Name: "semconv"},
57+
)
58+
f.SectionTemplates[0].Data.(map[string]any)["Imports"] = imports
59+
}
2860
return files, nil
2961
}

otel/generate_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ func TestOtel(t *testing.T) {
3737
fs, err := Generate("", []eval.Root{root}, serverFiles)
3838
assert.NoError(t, err)
3939
require.Len(t, fs, 2)
40-
// Generate is a no-op: the returned files must be identical
41-
// to the input server files.
42-
assert.Same(t, serverFiles[0], fs[0])
43-
assert.Same(t, serverFiles[1], fs[1])
4440
sections := fs[0].Section("server-handler")
4541
require.Len(t, sections, 1)
4642
section := sections[0]

otel/testdata/multiple routes.golden

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
77
h.ServeHTTP(w, r)
88
}
99
}
10-
mux.Handle("GET", "/", f)
11-
mux.Handle("GET", "/other", f)
10+
mux.Handle("GET", "/", func(w http.ResponseWriter, r *http.Request) {
11+
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/"))
12+
f(w, r)
13+
})
14+
mux.Handle("GET", "/other", func(w http.ResponseWriter, r *http.Request) {
15+
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/other"))
16+
f(w, r)
17+
})
1218
}

otel/testdata/one route.golden

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
77
h.ServeHTTP(w, r)
88
}
99
}
10-
mux.Handle("GET", "/", f)
10+
mux.Handle("GET", "/", func(w http.ResponseWriter, r *http.Request) {
11+
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute("/"))
12+
f(w, r)
13+
})
1114
}

0 commit comments

Comments
 (0)