diff --git a/CHANGELOG.md b/CHANGELOG.md index 245710157ac..cbff0a20bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The superfluous `response.WriteHeader` call in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` when the response writer is flushed. (#5634) - Custom attributes targeting metrics recorded by the `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` are not ignored anymore. (#5129) - +- Using `c.FullPath()` to set `http.route` attribute in [`otelgin`](go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin) +- Support pass `routeName` to `SpanNameFormatter` in [`otelgin`](go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin) ### Deprecated - The `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo` package is deprecated. diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go index e40775d7c3e..5a8ed0229b0 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go @@ -67,7 +67,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { if cfg.SpanNameFormatter == nil { spanName = c.FullPath() } else { - spanName = cfg.SpanNameFormatter(c.Request) + spanName = cfg.SpanNameFormatter(c.FullPath(), c.Request) } if spanName == "" { spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method) diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go index 113576ca00a..c3df9de84c3 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go @@ -23,8 +23,8 @@ type config struct { // be traced. A Filter must return true if the request should be traced. type Filter func(*http.Request) bool -// SpanNameFormatter is used to set span name by http.request. -type SpanNameFormatter func(r *http.Request) string +// SpanNameFormatter is used to set span name by http.Request. +type SpanNameFormatter func(routeName string, r *http.Request) string // Option specifies instrumentation configuration options. type Option interface { @@ -70,9 +70,11 @@ func WithFilter(f ...Filter) Option { }) } -// WithSpanNameFormatter takes a function that will be called on every -// request and the returned string will become the Span Name. -func WithSpanNameFormatter(f func(r *http.Request) string) Option { +// WithSpanNameFormatter specifies a function to use for generating a custom span +// name. By default, the route name (path template or regexp) is used. The route +// name is provided so you can use it in the span name without needing to +// duplicate the logic for extracting it from the request. +func WithSpanNameFormatter(f func(routeName string, r *http.Request) string) Option { return optionFunc(func(c *config) { c.SpanNameFormatter = f }) diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go index afd6460983a..5e1d7db93dc 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go @@ -7,6 +7,7 @@ package test import ( "errors" + "fmt" "html/template" "net/http" "net/http/httptest" @@ -160,7 +161,7 @@ func TestSpanName(t *testing.T) { wantSpanName string }{ {"/user/1", nil, "/user/:id"}, - {"/user/1", func(r *http.Request) string { return r.URL.Path }, "/user/1"}, + {"/user/1", func(route string, r *http.Request) string { return r.URL.Path }, "/user/1"}, } for _, tc := range testCases { t.Run(tc.requestPath, func(t *testing.T) { @@ -179,6 +180,39 @@ func TestSpanName(t *testing.T) { } } +func TestHTTPRouteWithSpanNameFormatter(t *testing.T) { + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + router := gin.New() + router.Use(otelgin.Middleware("foobar", + otelgin.WithTracerProvider(provider), + otelgin.WithSpanNameFormatter(func(routeName string, r *http.Request) string { + return fmt.Sprintf("%s %s", r.Method, routeName) + }), + )) + router.GET("/user/:id", func(c *gin.Context) { + id := c.Param("id") + _, _ = c.Writer.Write([]byte(id)) + }) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + // do and verify the request + router.ServeHTTP(w, r) + response := w.Result() + require.Equal(t, http.StatusOK, response.StatusCode) + + // verify traces look good + spans := sr.Ended() + span := spans[0] + assert.Equal(t, "GET /user/:id", span.Name()) + attr := span.Attributes() + assert.Contains(t, attr, attribute.String("http.method", "GET")) + assert.Contains(t, attr, attribute.String("http.route", "/user/:id")) +} + func TestHTML(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))