Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WithRouteTag adds HTTP route attribute to metrics #615

Merged
merged 12 commits into from
Jul 31, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 9 additions & 3 deletions instrumentation/net/http/otelhttp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
67 changes: 67 additions & 0 deletions instrumentation/net/http/otelhttp/test/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
pellared marked this conversation as resolved.
Show resolved Hide resolved
}
}
}