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
125 changes: 28 additions & 97 deletions otel/README.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,28 @@
# OpenTelemetry Plugin

The `otel` plugin is a [Goa](https://github.com/goadesign/goa/tree/v3) plugin that instruments
HTTP endpoints with OpenTelemetry configuring it with the endpoint route pattern. This
plugin is used in conjunction with the [Clue](https://github.com/goadesign/clue) project to
instrument services with OpenTelemetry.

## Usage

Simply import the plugin in the service design package. Use the blank identifier `_` as explicit
package name:

```go
package design

import . "goa.design/goa/v3/dsl"
import _ "goa.design/plugins/v3/otel" # Enables the otel plugin

var _ = API("...
```

and generate as usual:

```bash
goa gen PACKAGE
```

where `PACKAGE` is the Go import path of the design package.

## Effects of the Plugin

Importing the `otel` package changes the behavior of the `gen` command of the
`goa` tool. The `gen` command output is modified so that the generated HTTP
handlers are wrapped with a call to the `otelhttp.WithRouteTag`.

That is the code generated in `gen/http/<service>/server/server.go` that
mounts an endpoint handler onto the HTTP mux changes from:

```go
mux.Handle("<VERB>", "<PATH>", f)
```

to:

```go
mux.Handle("<VERB>", otelhttp.WithRouteTag("<PATH>", f).ServeHTTP))
```

## OpenTelemetry Configuration

For the code generated by the plugin to work the OpenTelemetry SDK must be
initialized. This can be done by importing the `clue` package and calling
`clue.ConfigureOpenTelemetry` in the `main` function of the service:

```go
package main

import (
"context"
"net/http"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"goa.design/clue/clue"
"goa.design/clue/log"
)

func main() {
// Create OpenTelemetry metric and span exporters
// Use stdout exporters for demo.
metricExporter, err := stdoutmetric.New()
if err != nil {
panic(err)
}
spanExporter, err := stdouttrace.New()
if err != nil {
panic(err)
}

// Configure OpenTelemetry.
ctx := log.Context(context.Background())
cfg, err := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter)
if err != nil {
panic(err)
}
clue.ConfigureOpenTelemetry(ctx, cfg)

// Create HTTP handler and start server.
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
handler = otelhttp.NewHandler(handler, "service") // Instrument handler.
http.ListenAndServe(":8080", handler)
}
```

# 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.
40 changes: 15 additions & 25 deletions otel/generate.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
// Package otel was a Goa plugin that instrumented HTTP handlers with
// otelhttp.WithRouteTag to set the http.route attribute on spans and metrics.
//
// 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 _ "goa.design/plugins/v3/otel" // ← delete this line
package otel

import (
"path/filepath"
"strings"

"goa.design/goa/v3/codegen"
"goa.design/goa/v3/eval"
)
Expand All @@ -13,27 +20,10 @@ func init() {
codegen.RegisterPluginLast("otel", "gen", nil, Generate)
}

// Generate generates the call to otelhttp.WithRouteTag
func Generate(genpkg string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
for _, f := range files {
if filepath.Base(f.Path) == "server.go" {
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 }}", otelhttp.WithRouteTag("{{ .Path }}", f).ServeHTTP)`,
1,
)
}
}
imports := f.SectionTemplates[0].Data.(map[string]any)["Imports"].([]*codegen.ImportSpec)
imports = append(imports, &codegen.ImportSpec{
Path: "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp",
Name: "otelhttp",
})
f.SectionTemplates[0].Data.(map[string]any)["Imports"] = imports
}
}
// 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) {
return files, nil
}
4 changes: 4 additions & 0 deletions otel/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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
5 changes: 2 additions & 3 deletions otel/testdata/multiple routes.golden
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// MountMethodHandler configures the mux to serve the "Service2" service
// "Method" endpoint.
func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
Expand All @@ -8,6 +7,6 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
h.ServeHTTP(w, r)
}
}
mux.Handle("GET", "/", otelhttp.WithRouteTag("/", f).ServeHTTP)
mux.Handle("GET", "/other", otelhttp.WithRouteTag("/other", f).ServeHTTP)
mux.Handle("GET", "/", f)
mux.Handle("GET", "/other", f)
}
3 changes: 1 addition & 2 deletions otel/testdata/one route.golden
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// MountMethodHandler configures the mux to serve the "Service" service
// "Method" endpoint.
func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
Expand All @@ -8,5 +7,5 @@ func MountMethodHandler(mux goahttp.Muxer, h http.Handler) {
h.ServeHTTP(w, r)
}
}
mux.Handle("GET", "/", otelhttp.WithRouteTag("/", f).ServeHTTP)
mux.Handle("GET", "/", f)
}
Loading