diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6160a68e2..89db3be6f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `WithAttributes` option to set instrumentation scope attributes on the created `log.Logger` in `go.opentelemetry.io/contrib/bridges/otelslog`. (#6965) - Add `WithAttributes` option to set instrumentation scope attributes on the created `log.Logger` in `go.opentelemetry.io/contrib/bridges/otellogrus`. (#6966) - Add `WithAttributes` option to set instrumentation scope attributes on the created `log.Logger` in `go.opentelemetry.io/contrib/bridges/otellogr`. (#6967) +- Add the `WithGinMetricAttributes` option to allow setting dynamic, per-request metric attributes based on `*gin.Context` in `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#6932) ### Changed diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/config.go b/instrumentation/github.com/gin-gonic/gin/otelgin/config.go index 23aad3b8537..f17e4302c7c 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/config.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/config.go @@ -17,13 +17,14 @@ import ( ) type config struct { - TracerProvider oteltrace.TracerProvider - Propagators propagation.TextMapPropagator - Filters []Filter - GinFilters []GinFilter - SpanNameFormatter SpanNameFormatter - MeterProvider metric.MeterProvider - MetricAttributeFn MetricAttributeFn + TracerProvider oteltrace.TracerProvider + Propagators propagation.TextMapPropagator + Filters []Filter + GinFilters []GinFilter + SpanNameFormatter SpanNameFormatter + MeterProvider metric.MeterProvider + MetricAttributeFn MetricAttributeFn + GinMetricAttributeFn GinMetricAttributeFn } // Filter is a predicate used to determine whether a given http.request should @@ -41,6 +42,10 @@ type SpanNameFormatter func(r *http.Request) string // and return them as a slice of attribute.KeyValue. type MetricAttributeFn func(*http.Request) []attribute.KeyValue +// GinMetricAttributeFn is used to extract additional attributes from the gin.Context +// and return them as a slice of attribute.KeyValue. +type GinMetricAttributeFn func(*gin.Context) []attribute.KeyValue + // Option specifies instrumentation configuration options. type Option interface { apply(*config) @@ -110,8 +115,20 @@ func WithMeterProvider(mp metric.MeterProvider) Option { // WithMetricAttributeFn specifies a function that extracts additional attributes from the http.Request // and returns them as a slice of attribute.KeyValue. +// +// If attributes are duplicated between this method and `WithGinMetricAttributeFn`, the attributes in this method will be overridden. func WithMetricAttributeFn(f MetricAttributeFn) Option { return optionFunc(func(c *config) { c.MetricAttributeFn = f }) } + +// WithGinMetricAttributeFn specifies a function that extracts additional attributes from the gin.Context +// and returns them as a slice of attribute.KeyValue. +// +// If attributes are duplicated between this method and `WithMetricAttributeFn`, the attributes in this method will be used. +func WithGinMetricAttributeFn(f GinMetricAttributeFn) Option { + return optionFunc(func(c *config) { + c.GinMetricAttributeFn = f + }) +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gin.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gin.go index 1b17df59fd5..68eb988c656 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gin.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gin.go @@ -118,7 +118,10 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { // Record the server-side attributes. var additionalAttributes []attribute.KeyValue if cfg.MetricAttributeFn != nil { - additionalAttributes = cfg.MetricAttributeFn(c.Request) + additionalAttributes = append(additionalAttributes, cfg.MetricAttributeFn(c.Request)...) + } + if cfg.GinMetricAttributeFn != nil { + additionalAttributes = append(additionalAttributes, cfg.GinMetricAttributeFn(c)...) } sc.RecordMetrics(ctx, semconv.ServerMetricData{ diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gin_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gin_test.go index b62cc5e394a..0a90143ab99 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gin_test.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gin_test.go @@ -439,17 +439,30 @@ func TestWithGinFilter(t *testing.T) { func TestMetrics(t *testing.T) { tests := []struct { - name string - metricAttributeExtractor func(*http.Request) []attribute.KeyValue + name string + metricAttributeExtractor func(*http.Request) []attribute.KeyValue + ginMetricAttributeExtractor func(*gin.Context) []attribute.KeyValue }{ - {"default", nil}, - {"with metric attributes callback", func(req *http.Request) []attribute.KeyValue { - return []attribute.KeyValue{ - attribute.String("key1", "value1"), - attribute.String("key2", "value"), - attribute.String("method", strings.ToUpper(req.Method)), - } - }}, + { + name: "default", + metricAttributeExtractor: nil, + ginMetricAttributeExtractor: nil, + }, + { + name: "with metric attributes callback", + metricAttributeExtractor: func(r *http.Request) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("key1", "value1"), + attribute.String("key2", "value"), + attribute.String("method", strings.ToUpper(r.Method)), + } + }, + ginMetricAttributeExtractor: func(c *gin.Context) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("key3", "value3"), + } + }, + }, } for _, tt := range tests { @@ -461,6 +474,7 @@ func TestMetrics(t *testing.T) { router.Use(otelgin.Middleware("foobar", otelgin.WithMeterProvider(meterProvider), otelgin.WithMetricAttributeFn(tt.metricAttributeExtractor), + otelgin.WithGinMetricAttributeFn(tt.ginMetricAttributeExtractor), )) router.GET("/user/:id", func(c *gin.Context) { id := c.Param("id") @@ -470,6 +484,8 @@ func TestMetrics(t *testing.T) { r := httptest.NewRequest("GET", "/user/123", nil) w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = r router.ServeHTTP(w, r) // verify metrics @@ -493,6 +509,9 @@ func TestMetrics(t *testing.T) { if tt.metricAttributeExtractor != nil { attrs = append(attrs, tt.metricAttributeExtractor(r)...) } + if tt.ginMetricAttributeExtractor != nil { + attrs = append(attrs, tt.ginMetricAttributeExtractor(c)...) + } metricdatatest.AssertEqual(t, metricdata.Metrics{ Name: "http.server.request.size",