diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f14b26dab..b5f38075690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `NewMiddleware` function in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#2964) - Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108) - The `go.opentelemetry.io/contrib/exporters/autoexport` package to provide configuration of trace exporters with useful defaults and envar support. (#2753, #4100) +- `WithRouteTag` in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` adds HTTP route attribute to metrics. (#615) ### Fixed diff --git a/instrumentation/net/http/otelhttp/handler.go b/instrumentation/net/http/otelhttp/handler.go index f103aa627d6..123365caed1 100644 --- a/instrumentation/net/http/otelhttp/handler.go +++ b/instrumentation/net/http/otelhttp/handler.go @@ -258,12 +258,18 @@ func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, span.SetAttributes(attributes...) } -// WithRouteTag annotates a span with the provided route name using the -// RouteKey Tag. +// WithRouteTag annotates spans and metrics with the provided route name +// with HTTP route attribute. func WithRouteTag(route string, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attr := semconv.HTTPRouteKey.String(route) + span := trace.SpanFromContext(r.Context()) - span.SetAttributes(semconv.HTTPRoute(route)) + span.SetAttributes(attr) + + labeler, _ := LabelerFromContext(r.Context()) + labeler.Add(attr) + h.ServeHTTP(w, r) }) } diff --git a/instrumentation/net/http/otelhttp/test/handler_test.go b/instrumentation/net/http/otelhttp/test/handler_test.go index a6e0ffd16f7..30eec741160 100644 --- a/instrumentation/net/http/otelhttp/test/handler_test.go +++ b/instrumentation/net/http/otelhttp/test/handler_test.go @@ -468,3 +468,70 @@ func TestSpanStatus(t *testing.T) { }) } } + +func TestWithRouteTag(t *testing.T) { + route := "/some/route" + + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider() + tracerProvider.RegisterSpanProcessor(spanRecorder) + + metricReader := metric.NewManualReader() + meterProvider := metric.NewMeterProvider(metric.WithReader(metricReader)) + + h := otelhttp.NewHandler( + otelhttp.WithRouteTag( + route, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + }), + ), + "test_handler", + otelhttp.WithTracerProvider(tracerProvider), + otelhttp.WithMeterProvider(meterProvider), + ) + + h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + want := semconv.HTTPRouteKey.String(route) + + require.Len(t, spanRecorder.Ended(), 1, "should emit a span") + gotSpan := spanRecorder.Ended()[0] + require.Contains(t, gotSpan.Attributes(), want, "should add route to span attributes") + + rm := metricdata.ResourceMetrics{} + err := metricReader.Collect(context.Background(), &rm) + require.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1, "should emit metrics for one scope") + gotMetrics := rm.ScopeMetrics[0].Metrics + + for _, m := range gotMetrics { + switch d := m.Data.(type) { + case metricdata.Sum[int64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + case metricdata.Sum[float64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + case metricdata.Histogram[int64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + case metricdata.Histogram[float64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + case metricdata.Gauge[int64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + case metricdata.Gauge[float64]: + require.Len(t, d.DataPoints, 1, "metric '%v' should have exactly one data point", m.Name) + require.Contains(t, d.DataPoints[0].Attributes.ToSlice(), want, "should add route to attributes for metric '%v'", m.Name) + + default: + require.Fail(t, "metric has unexpected data type", "metric '%v' has unexpected data type %T", m.Name, m.Data) + } + } +}