diff --git a/instrumentation/net/http/otelhttp/config.go b/instrumentation/net/http/otelhttp/config.go index f0a9bb9efeb..503bde27566 100644 --- a/instrumentation/net/http/otelhttp/config.go +++ b/instrumentation/net/http/otelhttp/config.go @@ -5,6 +5,7 @@ package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http import ( "context" + "go.opentelemetry.io/otel/attribute" "net/http" "net/http/httptrace" @@ -32,6 +33,7 @@ type config struct { Filters []Filter SpanNameFormatter func(string, *http.Request) string ClientTrace func(context.Context) *httptrace.ClientTrace + DefaultAttributes []attribute.KeyValue TracerProvider trace.TracerProvider MeterProvider metric.MeterProvider @@ -194,3 +196,11 @@ func WithServerName(server string) Option { c.ServerName = server }) } + +// WithDefaultAttributes returns an option that sets the default attributes to be +// included in metrics. +func WithDefaultAttributes(attributes []attribute.KeyValue) Option { + return optionFunc(func(c *config) { + c.DefaultAttributes = attributes + }) +} diff --git a/instrumentation/net/http/otelhttp/test/transport_test.go b/instrumentation/net/http/otelhttp/test/transport_test.go index cfbc27cc2b2..f44f3615f96 100644 --- a/instrumentation/net/http/otelhttp/test/transport_test.go +++ b/instrumentation/net/http/otelhttp/test/transport_test.go @@ -502,11 +502,14 @@ 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) @@ -528,30 +531,82 @@ 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.WithDefaultAttributes(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 c869f6d4bfc..feb1b465f44 100644 --- a/instrumentation/net/http/otelhttp/transport.go +++ b/instrumentation/net/http/otelhttp/transport.go @@ -33,6 +33,7 @@ type Transport struct { filters []Filter spanNameFormatter func(string, *http.Request) string clientTrace func(context.Context) *httptrace.ClientTrace + defaultAttributes []attribute.KeyValue requestBytesCounter metric.Int64Counter responseBytesCounter metric.Int64Counter @@ -76,6 +77,7 @@ func (t *Transport) applyConfig(c *config) { t.filters = c.Filters t.spanNameFormatter = c.SpanNameFormatter t.clientTrace = c.ClientTrace + t.defaultAttributes = c.DefaultAttributes } func (t *Transport) createMeasures() { @@ -167,7 +169,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.defaultAttributes...) if res.StatusCode > 0 { metricAttrs = append(metricAttrs, semconv.HTTPStatusCode(res.StatusCode)) }