diff --git a/instrumentation/github.com/aws/aws-lambda-go/otellambda/example/go.sum b/instrumentation/github.com/aws/aws-lambda-go/otellambda/example/go.sum index 04b0390180f..c8dc2e9aa20 100644 --- a/instrumentation/github.com/aws/aws-lambda-go/otellambda/example/go.sum +++ b/instrumentation/github.com/aws/aws-lambda-go/otellambda/example/go.sum @@ -68,6 +68,7 @@ go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ3 go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= diff --git a/instrumentation/net/http/otelhttp/go.mod b/instrumentation/net/http/otelhttp/go.mod index e11b1e82354..875e169bfb5 100644 --- a/instrumentation/net/http/otelhttp/go.mod +++ b/instrumentation/net/http/otelhttp/go.mod @@ -7,6 +7,7 @@ require ( github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/metric v1.21.0 + go.opentelemetry.io/otel/sdk/metric v1.21.0 go.opentelemetry.io/otel/trace v1.21.0 ) @@ -15,5 +16,7 @@ require ( github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + golang.org/x/sys v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/instrumentation/net/http/otelhttp/go.sum b/instrumentation/net/http/otelhttp/go.sum index 9bb1d84c4c2..32186e24d91 100644 --- a/instrumentation/net/http/otelhttp/go.sum +++ b/instrumentation/net/http/otelhttp/go.sum @@ -16,8 +16,14 @@ go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= +go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go index d9f0e91ac83..8694b17ec57 100644 --- a/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go @@ -52,10 +52,9 @@ func HTTPClientRequest(req *http.Request) []attribute.KeyValue { } // HTTPClientRequestMetrics returns metric attributes for an HTTP request made by a client. -// The following attributes are always returned: "http.flavor", -// "http.method", "net.peer.name". The following attributes are returned if the -// related values are defined in req: "http.url", "net.peer.port", "http.user_agent", -// "enduser.id". +// The following attributes are always returned: "http.method", "net.peer.name". +// The following attributes are returned if the +// related values are defined in req: "net.peer.port". func HTTPClientRequestMetrics(req *http.Request) []attribute.KeyValue { return hc.ClientRequestMetrics(req) } @@ -296,8 +295,8 @@ func (c *httpConv) ClientRequest(req *http.Request) []attribute.KeyValue { } // ClientRequestMetrics returns metric attributes for an HTTP request made by a client. The -// following attributes are always returned: "http.method", -// "net.peer.name". The following attributes are returned if the related values +// following attributes are always returned: "http.method", "net.peer.name". +// The following attributes are returned if the related values // are defined in req: "net.peer.port". func (c *httpConv) ClientRequestMetrics(req *http.Request) []attribute.KeyValue { /* The following semantic conventions are returned if present: diff --git a/instrumentation/net/http/otelhttp/transport_test.go b/instrumentation/net/http/otelhttp/transport_test.go index efa29d5435e..a01eb3fb4cd 100644 --- a/instrumentation/net/http/otelhttp/transport_test.go +++ b/instrumentation/net/http/otelhttp/transport_test.go @@ -18,9 +18,16 @@ import ( "bytes" "context" "errors" + "fmt" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" "io" + "net" "net/http" "net/http/httptest" + "strconv" "strings" "testing" @@ -30,6 +37,9 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" + + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func TestTransportFormatter(t *testing.T) { @@ -432,3 +442,121 @@ func TestTransportOriginRequestNotModify(t *testing.T) { assert.Equal(t, expectedRequest, r) } + +func TestTransportMetrics(t *testing.T) { + reader := metric.NewManualReader() + meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) + + responseBody := []byte("Hello, world!") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write(responseBody); err != nil { + t.Fatal(err) + } + })) + defer ts.Close() + + requestBody := []byte("john") + r, err := http.NewRequest(http.MethodGet, ts.URL, bytes.NewReader(requestBody)) + if err != nil { + t.Fatal(err) + } + + tr := NewTransport( + http.DefaultTransport, + WithMeterProvider(meterProvider), + ) + + c := http.Client{Transport: tr} + res, err := c.Do(r) + if err != nil { + t.Fatal(err) + } + + // Must read the body or else we won't get response metrics + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + require.Len(t, bodyBytes, 13) + require.NoError(t, res.Body.Close()) + + host, portStr, _ := net.SplitHostPort(r.Host) + if host == "" { + host = "127.0.0.1" + } + port, err := strconv.Atoi(portStr) + if err != nil { + port = 0 + } + + rm := metricdata.ResourceMetrics{} + err = reader.Collect(context.Background(), &rm) + require.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + attrs := attribute.NewSet( + semconv.NetPeerName(host), + semconv.NetPeerPort(port), + semconv.HTTPMethod("GET"), + semconv.HTTPStatusCode(200), + ) + assertClientScopeMetrics(t, rm.ScopeMetrics[0], attrs) +} + +func assertClientScopeMetrics(t *testing.T, sm metricdata.ScopeMetrics, attrs attribute.Set) { + + assert.Equal(t, instrumentation.Scope{ + Name: "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp", + Version: Version(), + }, sm.Scope) + + require.Len(t, sm.Metrics, 3) + + want := metricdata.Metrics{ + Name: "http.client.request_content_length", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: 4}}, + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + }, + Description: "Measures the size of HTTP request content length (uncompressed)", + Unit: "By", + } + metricdatatest.AssertEqual(t, want, sm.Metrics[0], metricdatatest.IgnoreTimestamp()) + + want = metricdata.Metrics{ + Name: "http.client.response_content_length", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: 13}}, + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + }, + Description: "Measures the size of HTTP response content length (uncompressed)", + Unit: "By", + } + metricdatatest.AssertEqual(t, want, sm.Metrics[1], metricdatatest.IgnoreTimestamp()) + + dur := sm.Metrics[2] + assert.Equal(t, "http.client.duration", dur.Name) + require.IsType(t, dur.Data, metricdata.Histogram[float64]{}) + hist := dur.Data.(metricdata.Histogram[float64]) + assert.Equal(t, metricdata.CumulativeTemporality, hist.Temporality) + require.Len(t, hist.DataPoints, 1) + dPt := hist.DataPoints[0] + assert.Equal(t, attrs, dPt.Attributes, "attributes") + assert.Equal(t, uint64(1), dPt.Count, "count") + assert.Equal(t, []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000}, dPt.Bounds, "bounds") + + t.Log(fmt.Sprintf("%+v", dPt.BucketCounts)) + + // Duration value is not deterministic because the code does not support a pluggable clock. + // So just value that one of the buckets has been incremented. + bucketSum := uint64(0) + for _, bucketCount := range dPt.BucketCounts { + bucketSum += bucketCount + } + + require.Equal(t, uint64(1), bucketSum) + +}