diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc84bc0f74..648874dda22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `go.opentelemetry.io/contrib/config` package supports configuring `with_resource_constant_labels` for the prometheus exporter. (#5890) - Add new runtime metrics to `go.opentelemetry.io/contrib/instrumentation/runtime`, which are still disabled by default. (#5870) - Support for the `OTEL_HTTP_CLIENT_COMPATIBILITY_MODE=http/dup` environment variable in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` to emit attributes for both the v1.20.0 and v1.26.0 semantic conventions. (#5401) +- Added option for adding default attributes to otel http transport in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#5876) +- Added option for extracting attributes from the http request in http transport in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#5876) ### Removed diff --git a/instrumentation/net/http/otelhttp/config.go b/instrumentation/net/http/otelhttp/config.go index f0a9bb9efeb..a01bfafbe07 100644 --- a/instrumentation/net/http/otelhttp/config.go +++ b/instrumentation/net/http/otelhttp/config.go @@ -8,6 +8,8 @@ import ( "net/http" "net/http/httptrace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" @@ -33,8 +35,9 @@ type config struct { SpanNameFormatter func(string, *http.Request) string ClientTrace func(context.Context) *httptrace.ClientTrace - TracerProvider trace.TracerProvider - MeterProvider metric.MeterProvider + TracerProvider trace.TracerProvider + MeterProvider metric.MeterProvider + MetricAttributesFn func(*http.Request) []attribute.KeyValue } // Option interface used for setting optional config properties. @@ -194,3 +197,11 @@ func WithServerName(server string) Option { c.ServerName = server }) } + +// WithMetricAttributesFn returns an Option to set a function that maps an HTTP request to a slice of attribute.KeyValue. +// These attributes will be included in metrics for every request. +func WithMetricAttributesFn(metricAttributesFn func(r *http.Request) []attribute.KeyValue) Option { + return optionFunc(func(c *config) { + c.MetricAttributesFn = metricAttributesFn + }) +} diff --git a/instrumentation/net/http/otelhttp/test/transport_test.go b/instrumentation/net/http/otelhttp/test/transport_test.go index e61950dd7dd..a5963008819 100644 --- a/instrumentation/net/http/otelhttp/test/transport_test.go +++ b/instrumentation/net/http/otelhttp/test/transport_test.go @@ -507,11 +507,15 @@ func TestCustomAttributesHandling(t *testing.T) { })) defer ts.Close() + expectedAttributes := []attribute.KeyValue{ + attribute.String("foo", "fooValue"), + attribute.String("bar", "barValue"), + } + r, err := http.NewRequest(http.MethodGet, ts.URL, nil) require.NoError(t, err) labeler := &otelhttp.Labeler{} - labeler.Add(attribute.String("foo", "fooValue")) - labeler.Add(attribute.String("bar", "barValue")) + labeler.Add(expectedAttributes...) ctx = otelhttp.ContextWithLabeler(ctx, labeler) r = r.WithContext(ctx) @@ -534,30 +538,85 @@ func TestCustomAttributesHandling(t *testing.T) { d, ok := m.Data.(metricdata.Sum[int64]) assert.True(t, ok) assert.Len(t, d.DataPoints, 1) - attrSet := d.DataPoints[0].Attributes - fooAtrr, ok := attrSet.Value(attribute.Key("foo")) - assert.True(t, ok) - assert.Equal(t, "fooValue", fooAtrr.AsString()) - barAtrr, ok := attrSet.Value(attribute.Key("bar")) - assert.True(t, ok) - assert.Equal(t, "barValue", barAtrr.AsString()) - assert.False(t, attrSet.HasValue(attribute.Key("baz"))) + containsAttributes(t, d.DataPoints[0].Attributes, expectedAttributes) case clientDuration: d, ok := m.Data.(metricdata.Histogram[float64]) assert.True(t, ok) assert.Len(t, d.DataPoints, 1) - attrSet := d.DataPoints[0].Attributes - fooAtrr, ok := attrSet.Value(attribute.Key("foo")) + containsAttributes(t, d.DataPoints[0].Attributes, expectedAttributes) + } + } +} + +func TestDefaultAttributesHandling(t *testing.T) { + var rm metricdata.ResourceMetrics + const ( + clientRequestSize = "http.client.request.size" + clientDuration = "http.client.duration" + ) + ctx := context.TODO() + reader := sdkmetric.NewManualReader() + provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + defer func() { + err := provider.Shutdown(ctx) + if err != nil { + t.Errorf("Error shutting down provider: %v", err) + } + }() + + defaultAttributes := []attribute.KeyValue{ + attribute.String("defaultFoo", "fooValue"), + attribute.String("defaultBar", "barValue"), + } + + transport := otelhttp.NewTransport( + http.DefaultTransport, otelhttp.WithMeterProvider(provider), + otelhttp.WithMetricAttributesFn(func(_ *http.Request) []attribute.KeyValue { + return defaultAttributes + })) + client := http.Client{Transport: transport} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + r, err := http.NewRequest(http.MethodGet, ts.URL, nil) + require.NoError(t, err) + + _, err = client.Do(r) + require.NoError(t, err) + + err = reader.Collect(ctx, &rm) + assert.NoError(t, err) + + // http.client.response.size is not recorded so the assert.Len + // above should be 2 instead of 3(test bonus) + assert.Len(t, rm.ScopeMetrics[0].Metrics, 2) + for _, m := range rm.ScopeMetrics[0].Metrics { + switch m.Name { + case clientRequestSize: + d, ok := m.Data.(metricdata.Sum[int64]) assert.True(t, ok) - assert.Equal(t, "fooValue", fooAtrr.AsString()) - barAtrr, ok := attrSet.Value(attribute.Key("bar")) + assert.Len(t, d.DataPoints, 1) + containsAttributes(t, d.DataPoints[0].Attributes, defaultAttributes) + case clientDuration: + d, ok := m.Data.(metricdata.Histogram[float64]) assert.True(t, ok) - assert.Equal(t, "barValue", barAtrr.AsString()) - assert.False(t, attrSet.HasValue(attribute.Key("baz"))) + assert.Len(t, d.DataPoints, 1) + containsAttributes(t, d.DataPoints[0].Attributes, defaultAttributes) } } } +func containsAttributes(t *testing.T, attrSet attribute.Set, expected []attribute.KeyValue) { + for _, att := range expected { + actualValue, ok := attrSet.Value(att.Key) + assert.True(t, ok) + assert.Equal(t, att.Value.AsString(), actualValue.AsString()) + } +} + func BenchmarkTransportRoundTrip(b *testing.B) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello World") diff --git a/instrumentation/net/http/otelhttp/transport.go b/instrumentation/net/http/otelhttp/transport.go index fc4dd98f3d0..b4119d3438b 100644 --- a/instrumentation/net/http/otelhttp/transport.go +++ b/instrumentation/net/http/otelhttp/transport.go @@ -28,13 +28,14 @@ import ( type Transport struct { rt http.RoundTripper - tracer trace.Tracer - meter metric.Meter - propagators propagation.TextMapPropagator - spanStartOptions []trace.SpanStartOption - filters []Filter - spanNameFormatter func(string, *http.Request) string - clientTrace func(context.Context) *httptrace.ClientTrace + tracer trace.Tracer + meter metric.Meter + propagators propagation.TextMapPropagator + spanStartOptions []trace.SpanStartOption + filters []Filter + spanNameFormatter func(string, *http.Request) string + clientTrace func(context.Context) *httptrace.ClientTrace + metricAttributesFn func(*http.Request) []attribute.KeyValue semconv semconv.HTTPClient requestBytesCounter metric.Int64Counter @@ -80,6 +81,7 @@ func (t *Transport) applyConfig(c *config) { t.filters = c.Filters t.spanNameFormatter = c.SpanNameFormatter t.clientTrace = c.ClientTrace + t.metricAttributesFn = c.MetricAttributesFn } func (t *Transport) createMeasures() { @@ -175,7 +177,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { } // metrics - metricAttrs := append(labeler.Get(), semconvutil.HTTPClientRequestMetrics(r)...) + metricAttrs := append(append(labeler.Get(), semconvutil.HTTPClientRequestMetrics(r)...), t.metricAttributesFromRequest(r)...) if res.StatusCode > 0 { metricAttrs = append(metricAttrs, semconv.HTTPStatusCode(res.StatusCode)) } @@ -201,6 +203,14 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { return res, err } +func (t *Transport) metricAttributesFromRequest(r *http.Request) []attribute.KeyValue { + var attributeForRequest []attribute.KeyValue + if t.metricAttributesFn != nil { + attributeForRequest = t.metricAttributesFn(r) + } + return attributeForRequest +} + // newWrappedBody returns a new and appropriately scoped *wrappedBody as an // io.ReadCloser. If the passed body implements io.Writer, the returned value // will implement io.ReadWriteCloser.