Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 50 additions & 28 deletions otel/README.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 45 additions & 13 deletions otel/generate.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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
}
4 changes: 0 additions & 4 deletions otel/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 8 additions & 2 deletions otel/testdata/multiple routes.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
5 changes: 4 additions & 1 deletion otel/testdata/one route.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Loading