From 5139bd78a1ffaeaecc3ebb275ee151f253f168e0 Mon Sep 17 00:00:00 2001 From: Damien Mathieu <42@dmathieu.com> Date: Tue, 10 Dec 2024 20:16:40 +0100 Subject: [PATCH] Switch the otelhttp semconv tests to use metricdatatest (#6400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This switches the otelhttp semconv tests to use the [metricdatatest](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest) package rather than a custom struct with a partial metrics implementation. This is meant to make it easier to start implementing the client and server metrics. --------- Co-authored-by: Robert PajÄ…k Co-authored-by: Tyler Yahn --- .../otelhttp/internal/semconv/common_test.go | 155 --------- .../net/http/otelhttp/internal/semconv/env.go | 28 +- .../otelhttp/internal/semconv/env_test.go | 42 --- .../otelhttp/internal/semconv/httpconv.go | 34 +- .../internal/semconv/httpconv_test.go | 257 +++++---------- .../internal/semconv/test/common_test.go | 68 ++++ .../otelhttp/internal/semconv/test/go.mod | 25 ++ .../otelhttp/internal/semconv/test/go.sum | 31 ++ .../internal/semconv/test/httpconv_test.go | 192 +++++++++++ .../internal/semconv/test/v1.20.0_test.go | 308 ++++++++++++++++++ .../http/otelhttp/internal/semconv/util.go | 4 +- .../otelhttp/internal/semconv/util_test.go | 2 +- .../http/otelhttp/internal/semconv/v1.20.0.go | 32 +- .../otelhttp/internal/semconv/v1.20.0_test.go | 188 ----------- 14 files changed, 758 insertions(+), 608 deletions(-) delete mode 100644 instrumentation/net/http/otelhttp/internal/semconv/common_test.go create mode 100644 instrumentation/net/http/otelhttp/internal/semconv/test/common_test.go create mode 100644 instrumentation/net/http/otelhttp/internal/semconv/test/go.mod create mode 100644 instrumentation/net/http/otelhttp/internal/semconv/test/go.sum create mode 100644 instrumentation/net/http/otelhttp/internal/semconv/test/httpconv_test.go create mode 100644 instrumentation/net/http/otelhttp/internal/semconv/test/v1.20.0_test.go diff --git a/instrumentation/net/http/otelhttp/internal/semconv/common_test.go b/instrumentation/net/http/otelhttp/internal/semconv/common_test.go deleted file mode 100644 index c6b373b6c37..00000000000 --- a/instrumentation/net/http/otelhttp/internal/semconv/common_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package semconv - -import ( - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" -) - -type testServerReq struct { - hostname string - serverPort int - peerAddr string - peerPort int - clientIP string -} - -func testTraceRequest(t *testing.T, serv HTTPServer, want func(testServerReq) []attribute.KeyValue) { - t.Helper() - - got := make(chan *http.Request, 1) - handler := func(w http.ResponseWriter, r *http.Request) { - got <- r - close(got) - w.WriteHeader(http.StatusOK) - } - - srv := httptest.NewServer(http.HandlerFunc(handler)) - defer srv.Close() - - srvURL, err := url.Parse(srv.URL) - require.NoError(t, err) - srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) - require.NoError(t, err) - - resp, err := srv.Client().Get(srv.URL) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - - req := <-got - peer, peerPort := splitHostPort(req.RemoteAddr) - - const user = "alice" - req.SetBasicAuth(user, "pswrd") - - const clientIP = "127.0.0.5" - req.Header.Add("X-Forwarded-For", clientIP) - - srvReq := testServerReq{ - hostname: srvURL.Hostname(), - serverPort: int(srvPort), - peerAddr: peer, - peerPort: peerPort, - clientIP: clientIP, - } - - assert.ElementsMatch(t, want(srvReq), serv.RequestTraceAttrs("", req)) -} - -func TestHTTPClientStatus(t *testing.T) { - tests := []struct { - code int - stat codes.Code - msg bool - }{ - {0, codes.Error, true}, - {http.StatusContinue, codes.Unset, false}, - {http.StatusSwitchingProtocols, codes.Unset, false}, - {http.StatusProcessing, codes.Unset, false}, - {http.StatusEarlyHints, codes.Unset, false}, - {http.StatusOK, codes.Unset, false}, - {http.StatusCreated, codes.Unset, false}, - {http.StatusAccepted, codes.Unset, false}, - {http.StatusNonAuthoritativeInfo, codes.Unset, false}, - {http.StatusNoContent, codes.Unset, false}, - {http.StatusResetContent, codes.Unset, false}, - {http.StatusPartialContent, codes.Unset, false}, - {http.StatusMultiStatus, codes.Unset, false}, - {http.StatusAlreadyReported, codes.Unset, false}, - {http.StatusIMUsed, codes.Unset, false}, - {http.StatusMultipleChoices, codes.Unset, false}, - {http.StatusMovedPermanently, codes.Unset, false}, - {http.StatusFound, codes.Unset, false}, - {http.StatusSeeOther, codes.Unset, false}, - {http.StatusNotModified, codes.Unset, false}, - {http.StatusUseProxy, codes.Unset, false}, - {306, codes.Unset, false}, - {http.StatusTemporaryRedirect, codes.Unset, false}, - {http.StatusPermanentRedirect, codes.Unset, false}, - {http.StatusBadRequest, codes.Error, false}, - {http.StatusUnauthorized, codes.Error, false}, - {http.StatusPaymentRequired, codes.Error, false}, - {http.StatusForbidden, codes.Error, false}, - {http.StatusNotFound, codes.Error, false}, - {http.StatusMethodNotAllowed, codes.Error, false}, - {http.StatusNotAcceptable, codes.Error, false}, - {http.StatusProxyAuthRequired, codes.Error, false}, - {http.StatusRequestTimeout, codes.Error, false}, - {http.StatusConflict, codes.Error, false}, - {http.StatusGone, codes.Error, false}, - {http.StatusLengthRequired, codes.Error, false}, - {http.StatusPreconditionFailed, codes.Error, false}, - {http.StatusRequestEntityTooLarge, codes.Error, false}, - {http.StatusRequestURITooLong, codes.Error, false}, - {http.StatusUnsupportedMediaType, codes.Error, false}, - {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, - {http.StatusExpectationFailed, codes.Error, false}, - {http.StatusTeapot, codes.Error, false}, - {http.StatusMisdirectedRequest, codes.Error, false}, - {http.StatusUnprocessableEntity, codes.Error, false}, - {http.StatusLocked, codes.Error, false}, - {http.StatusFailedDependency, codes.Error, false}, - {http.StatusTooEarly, codes.Error, false}, - {http.StatusUpgradeRequired, codes.Error, false}, - {http.StatusPreconditionRequired, codes.Error, false}, - {http.StatusTooManyRequests, codes.Error, false}, - {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, - {http.StatusUnavailableForLegalReasons, codes.Error, false}, - {499, codes.Error, false}, - {http.StatusInternalServerError, codes.Error, false}, - {http.StatusNotImplemented, codes.Error, false}, - {http.StatusBadGateway, codes.Error, false}, - {http.StatusServiceUnavailable, codes.Error, false}, - {http.StatusGatewayTimeout, codes.Error, false}, - {http.StatusHTTPVersionNotSupported, codes.Error, false}, - {http.StatusVariantAlsoNegotiates, codes.Error, false}, - {http.StatusInsufficientStorage, codes.Error, false}, - {http.StatusLoopDetected, codes.Error, false}, - {http.StatusNotExtended, codes.Error, false}, - {http.StatusNetworkAuthenticationRequired, codes.Error, false}, - {600, codes.Error, true}, - } - - for _, test := range tests { - t.Run(strconv.Itoa(test.code), func(t *testing.T) { - c, msg := HTTPClient{}.Status(test.code) - assert.Equal(t, test.stat, c) - if test.msg && msg == "" { - t.Errorf("expected non-empty message for %d", test.code) - } else if !test.msg && msg != "" { - t.Errorf("expected empty message for %d, got: %s", test.code, msg) - } - }) - } -} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/env.go b/instrumentation/net/http/otelhttp/internal/semconv/env.go index fec528536a5..bbbfeddfc21 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/env.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/env.go @@ -50,9 +50,9 @@ type HTTPServer struct { // The req Host will be used to determine the server instead. func (s HTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { if s.duplicate { - return append(oldHTTPServer{}.RequestTraceAttrs(server, req), newHTTPServer{}.RequestTraceAttrs(server, req)...) + return append(OldHTTPServer{}.RequestTraceAttrs(server, req), CurrentHTTPServer{}.RequestTraceAttrs(server, req)...) } - return oldHTTPServer{}.RequestTraceAttrs(server, req) + return OldHTTPServer{}.RequestTraceAttrs(server, req) } // ResponseTraceAttrs returns trace attributes for telemetry from an HTTP response. @@ -60,14 +60,14 @@ func (s HTTPServer) RequestTraceAttrs(server string, req *http.Request) []attrib // If any of the fields in the ResponseTelemetry are not set the attribute will be omitted. func (s HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { if s.duplicate { - return append(oldHTTPServer{}.ResponseTraceAttrs(resp), newHTTPServer{}.ResponseTraceAttrs(resp)...) + return append(OldHTTPServer{}.ResponseTraceAttrs(resp), CurrentHTTPServer{}.ResponseTraceAttrs(resp)...) } - return oldHTTPServer{}.ResponseTraceAttrs(resp) + return OldHTTPServer{}.ResponseTraceAttrs(resp) } // Route returns the attribute for the route. func (s HTTPServer) Route(route string) attribute.KeyValue { - return oldHTTPServer{}.Route(route) + return OldHTTPServer{}.Route(route) } // Status returns a span status code and message for an HTTP status code @@ -108,7 +108,7 @@ func (s HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) { return } - attributes := oldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes) + attributes := OldHTTPServer{}.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.AdditionalAttributes) o := metric.WithAttributeSet(attribute.NewSet(attributes...)) addOpts := []metric.AddOption{o} s.requestBytesCounter.Add(ctx, md.RequestSize, addOpts...) @@ -124,7 +124,7 @@ func NewHTTPServer(meter metric.Meter) HTTPServer { server := HTTPServer{ duplicate: duplicate, } - server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = oldHTTPServer{}.createMeasures(meter) + server.requestBytesCounter, server.responseBytesCounter, server.serverLatencyMeasure = OldHTTPServer{}.createMeasures(meter) return server } @@ -142,25 +142,25 @@ func NewHTTPClient(meter metric.Meter) HTTPClient { client := HTTPClient{ duplicate: env == "http/dup", } - client.requestBytesCounter, client.responseBytesCounter, client.latencyMeasure = oldHTTPClient{}.createMeasures(meter) + client.requestBytesCounter, client.responseBytesCounter, client.latencyMeasure = OldHTTPClient{}.createMeasures(meter) return client } // RequestTraceAttrs returns attributes for an HTTP request made by a client. func (c HTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { if c.duplicate { - return append(oldHTTPClient{}.RequestTraceAttrs(req), newHTTPClient{}.RequestTraceAttrs(req)...) + return append(OldHTTPClient{}.RequestTraceAttrs(req), CurrentHTTPClient{}.RequestTraceAttrs(req)...) } - return oldHTTPClient{}.RequestTraceAttrs(req) + return OldHTTPClient{}.RequestTraceAttrs(req) } // ResponseTraceAttrs returns metric attributes for an HTTP request made by a client. func (c HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { if c.duplicate { - return append(oldHTTPClient{}.ResponseTraceAttrs(resp), newHTTPClient{}.ResponseTraceAttrs(resp)...) + return append(OldHTTPClient{}.ResponseTraceAttrs(resp), CurrentHTTPClient{}.ResponseTraceAttrs(resp)...) } - return oldHTTPClient{}.ResponseTraceAttrs(resp) + return OldHTTPClient{}.ResponseTraceAttrs(resp) } func (c HTTPClient) Status(code int) (codes.Code, string) { @@ -175,7 +175,7 @@ func (c HTTPClient) Status(code int) (codes.Code, string) { func (c HTTPClient) ErrorType(err error) attribute.KeyValue { if c.duplicate { - return newHTTPClient{}.ErrorType(err) + return CurrentHTTPClient{}.ErrorType(err) } return attribute.KeyValue{} @@ -195,7 +195,7 @@ func (o MetricOpts) AddOptions() metric.AddOption { } func (c HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts { - attributes := oldHTTPClient{}.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) + attributes := OldHTTPClient{}.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) // TODO: Duplicate Metrics set := metric.WithAttributeSet(attribute.NewSet(attributes...)) return MetricOpts{ diff --git a/instrumentation/net/http/otelhttp/internal/semconv/env_test.go b/instrumentation/net/http/otelhttp/internal/semconv/env_test.go index 3a02a777373..3dd2c96509c 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/env_test.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/env_test.go @@ -10,9 +10,6 @@ import ( "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/metric/embedded" "go.opentelemetry.io/otel/metric/noop" ) @@ -93,42 +90,3 @@ func TestHTTPClientDoesNotPanic(t *testing.T) { }) } } - -type testInst struct { - embedded.Int64Counter - embedded.Float64Histogram - - intValue int64 - floatValue float64 - attributes []attribute.KeyValue -} - -func (t *testInst) Add(ctx context.Context, incr int64, options ...metric.AddOption) { - t.intValue = incr - cfg := metric.NewAddConfig(options) - attr := cfg.Attributes() - t.attributes = attr.ToSlice() -} - -func (t *testInst) Record(ctx context.Context, value float64, options ...metric.RecordOption) { - t.floatValue = value - cfg := metric.NewRecordConfig(options) - attr := cfg.Attributes() - t.attributes = attr.ToSlice() -} - -func NewTestHTTPServer() HTTPServer { - return HTTPServer{ - requestBytesCounter: &testInst{}, - responseBytesCounter: &testInst{}, - serverLatencyMeasure: &testInst{}, - } -} - -func NewTestHTTPClient() HTTPClient { - return HTTPClient{ - requestBytesCounter: &testInst{}, - responseBytesCounter: &testInst{}, - latencyMeasure: &testInst{}, - } -} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go b/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go index 745b8c67bc4..dc9ec7bc39e 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/httpconv.go @@ -14,7 +14,7 @@ import ( semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0" ) -type newHTTPServer struct{} +type CurrentHTTPServer struct{} // TraceRequest returns trace attributes for an HTTP request received by a // server. @@ -32,18 +32,18 @@ type newHTTPServer struct{} // // If the primary server name is not known, server should be an empty string. // The req Host will be used to determine the server instead. -func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { +func (n CurrentHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { count := 3 // ServerAddress, Method, Scheme var host string var p int if server == "" { - host, p = splitHostPort(req.Host) + host, p = SplitHostPort(req.Host) } else { // Prioritize the primary server name. - host, p = splitHostPort(server) + host, p = SplitHostPort(server) if p < 0 { - _, p = splitHostPort(req.Host) + _, p = SplitHostPort(req.Host) } } @@ -59,7 +59,7 @@ func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []att scheme := n.scheme(req.TLS != nil) - if peer, peerPort := splitHostPort(req.RemoteAddr); peer != "" { + if peer, peerPort := SplitHostPort(req.RemoteAddr); peer != "" { // The Go HTTP server sets RemoteAddr to "IP:port", this will not be a // file-path that would be interpreted with a sock family. count++ @@ -104,7 +104,7 @@ func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []att attrs = append(attrs, methodOriginal) } - if peer, peerPort := splitHostPort(req.RemoteAddr); peer != "" { + if peer, peerPort := SplitHostPort(req.RemoteAddr); peer != "" { // The Go HTTP server sets RemoteAddr to "IP:port", this will not be a // file-path that would be interpreted with a sock family. attrs = append(attrs, semconvNew.NetworkPeerAddress(peer)) @@ -135,7 +135,7 @@ func (n newHTTPServer) RequestTraceAttrs(server string, req *http.Request) []att return attrs } -func (n newHTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) { +func (n CurrentHTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) { if method == "" { return semconvNew.HTTPRequestMethodGet, attribute.KeyValue{} } @@ -150,7 +150,7 @@ func (n newHTTPServer) method(method string) (attribute.KeyValue, attribute.KeyV return semconvNew.HTTPRequestMethodGet, orig } -func (n newHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive +func (n CurrentHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive if https { return semconvNew.URLScheme("https") } @@ -160,7 +160,7 @@ func (n newHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive // TraceResponse returns trace attributes for telemetry from an HTTP response. // // If any of the fields in the ResponseTelemetry are not set the attribute will be omitted. -func (n newHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { +func (n CurrentHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { var count int if resp.ReadBytes > 0 { @@ -195,14 +195,14 @@ func (n newHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.Ke } // Route returns the attribute for the route. -func (n newHTTPServer) Route(route string) attribute.KeyValue { +func (n CurrentHTTPServer) Route(route string) attribute.KeyValue { return semconvNew.HTTPRoute(route) } -type newHTTPClient struct{} +type CurrentHTTPClient struct{} // RequestTraceAttrs returns trace attributes for an HTTP request made by a client. -func (n newHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { +func (n CurrentHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { /* below attributes are returned: - http.request.method @@ -222,7 +222,7 @@ func (n newHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue var requestHost string var requestPort int for _, hostport := range []string{urlHost, req.Header.Get("Host")} { - requestHost, requestPort = splitHostPort(hostport) + requestHost, requestPort = SplitHostPort(hostport) if requestHost != "" || requestPort > 0 { break } @@ -284,7 +284,7 @@ func (n newHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue } // ResponseTraceAttrs returns trace attributes for an HTTP response made by a client. -func (n newHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { +func (n CurrentHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { /* below attributes are returned: - http.response.status_code @@ -311,7 +311,7 @@ func (n newHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyVa return attrs } -func (n newHTTPClient) ErrorType(err error) attribute.KeyValue { +func (n CurrentHTTPClient) ErrorType(err error) attribute.KeyValue { t := reflect.TypeOf(err) var value string if t.PkgPath() == "" && t.Name() == "" { @@ -328,7 +328,7 @@ func (n newHTTPClient) ErrorType(err error) attribute.KeyValue { return semconvNew.ErrorTypeKey.String(value) } -func (n newHTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) { +func (n CurrentHTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) { if method == "" { return semconvNew.HTTPRequestMethodGet, attribute.KeyValue{} } diff --git a/instrumentation/net/http/otelhttp/internal/semconv/httpconv_test.go b/instrumentation/net/http/otelhttp/internal/semconv/httpconv_test.go index 6a3f6c09a4f..47b4db548ce 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/httpconv_test.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/httpconv_test.go @@ -4,97 +4,16 @@ package semconv import ( - "errors" - "fmt" "net/http" - "net/http/httptest" - "strings" + "strconv" "testing" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" ) -func TestNewTraceRequest(t *testing.T) { - t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") - serv := NewHTTPServer(nil) - want := func(req testServerReq) []attribute.KeyValue { - return []attribute.KeyValue{ - attribute.String("http.request.method", "GET"), - attribute.String("url.scheme", "http"), - attribute.String("server.address", req.hostname), - attribute.Int("server.port", req.serverPort), - attribute.String("network.peer.address", req.peerAddr), - attribute.Int("network.peer.port", req.peerPort), - attribute.String("user_agent.original", "Go-http-client/1.1"), - attribute.String("client.address", req.clientIP), - attribute.String("network.protocol.version", "1.1"), - attribute.String("url.path", "/"), - attribute.String("http.method", "GET"), - attribute.String("http.scheme", "http"), - attribute.String("net.host.name", req.hostname), - attribute.Int("net.host.port", req.serverPort), - attribute.String("net.sock.peer.addr", req.peerAddr), - attribute.Int("net.sock.peer.port", req.peerPort), - attribute.String("user_agent.original", "Go-http-client/1.1"), - attribute.String("http.client_ip", req.clientIP), - attribute.String("net.protocol.version", "1.1"), - attribute.String("http.target", "/"), - } - } - testTraceRequest(t, serv, want) -} - -func TestNewTraceResponse(t *testing.T) { - testCases := []struct { - name string - resp ResponseTelemetry - want []attribute.KeyValue - }{ - { - name: "empty", - resp: ResponseTelemetry{}, - want: nil, - }, - { - name: "no errors", - resp: ResponseTelemetry{ - StatusCode: 200, - ReadBytes: 701, - WriteBytes: 802, - }, - want: []attribute.KeyValue{ - attribute.Int("http.request.body.size", 701), - attribute.Int("http.response.body.size", 802), - attribute.Int("http.response.status_code", 200), - }, - }, - { - name: "with errors", - resp: ResponseTelemetry{ - StatusCode: 200, - ReadBytes: 701, - ReadError: fmt.Errorf("read error"), - WriteBytes: 802, - WriteError: fmt.Errorf("write error"), - }, - want: []attribute.KeyValue{ - attribute.Int("http.request.body.size", 701), - attribute.Int("http.response.body.size", 802), - attribute.Int("http.response.status_code", 200), - }, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - got := newHTTPServer{}.ResponseTraceAttrs(tt.resp) - assert.ElementsMatch(t, tt.want, got) - }) - } -} - func TestNewMethod(t *testing.T) { testCases := []struct { method string @@ -123,104 +42,96 @@ func TestNewMethod(t *testing.T) { for _, tt := range testCases { t.Run(tt.method, func(t *testing.T) { - got, gotOrig := newHTTPServer{}.method(tt.method) + got, gotOrig := CurrentHTTPServer{}.method(tt.method) assert.Equal(t, tt.want, got) assert.Equal(t, tt.wantOrig, gotOrig) }) } } -func TestNewTraceRequest_Client(t *testing.T) { - t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") - body := strings.NewReader("Hello, world!") - url := "https://example.com:8888/foo/bar?stuff=morestuff" - req := httptest.NewRequest("pOST", url, body) - req.Header.Set("User-Agent", "go-test-agent") - - want := []attribute.KeyValue{ - attribute.String("http.request.method", "POST"), - attribute.String("http.request.method_original", "pOST"), - attribute.String("http.method", "pOST"), - attribute.String("url.full", url), - attribute.String("http.url", url), - attribute.String("server.address", "example.com"), - attribute.Int("server.port", 8888), - attribute.String("network.protocol.version", "1.1"), - attribute.String("net.peer.name", "example.com"), - attribute.Int("net.peer.port", 8888), - attribute.String("user_agent.original", "go-test-agent"), - attribute.Int("http.request_content_length", 13), - } - client := NewHTTPClient(nil) - assert.ElementsMatch(t, want, client.RequestTraceAttrs(req)) -} - -func TestNewTraceResponse_Client(t *testing.T) { - t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") - testcases := []struct { - resp http.Response - want []attribute.KeyValue - }{ - {resp: http.Response{StatusCode: 200, ContentLength: 123}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 200), attribute.Int("http.status_code", 200), attribute.Int("http.response_content_length", 123)}}, - {resp: http.Response{StatusCode: 404, ContentLength: 0}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 404), attribute.Int("http.status_code", 404), attribute.String("error.type", "404")}}, - } - - for _, tt := range testcases { - client := NewHTTPClient(nil) - assert.ElementsMatch(t, tt.want, client.ResponseTraceAttrs(&tt.resp)) - } -} - -func TestClientRequest(t *testing.T) { - body := strings.NewReader("Hello, world!") - url := "https://example.com:8888/foo/bar?stuff=morestuff" - req := httptest.NewRequest("pOST", url, body) - req.Header.Set("User-Agent", "go-test-agent") - - want := []attribute.KeyValue{ - attribute.String("http.request.method", "POST"), - attribute.String("http.request.method_original", "pOST"), - attribute.String("url.full", url), - attribute.String("server.address", "example.com"), - attribute.Int("server.port", 8888), - attribute.String("network.protocol.version", "1.1"), - } - got := newHTTPClient{}.RequestTraceAttrs(req) - assert.ElementsMatch(t, want, got) -} - -func TestClientResponse(t *testing.T) { - testcases := []struct { - resp http.Response - want []attribute.KeyValue +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool }{ - {resp: http.Response{StatusCode: 200, ContentLength: 123}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 200)}}, - {resp: http.Response{StatusCode: 404, ContentLength: 0}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 404), attribute.String("error.type", "404")}}, + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Unset, false}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {499, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, } - for _, tt := range testcases { - got := newHTTPClient{}.ResponseTraceAttrs(&tt.resp) - assert.ElementsMatch(t, tt.want, got) - } -} - -func TestRequestErrorType(t *testing.T) { - testcases := []struct { - err error - want attribute.KeyValue - }{ - {err: errors.New("http: nil Request.URL"), want: attribute.String("error.type", "*errors.errorString")}, - {err: customError{}, want: attribute.String("error.type", "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv.customError")}, - } - - for _, tt := range testcases { - got := newHTTPClient{}.ErrorType(tt.err) - assert.Equal(t, tt.want, got) + for _, test := range tests { + t.Run(strconv.Itoa(test.code), func(t *testing.T) { + c, msg := HTTPClient{}.Status(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + }) } } - -type customError struct{} - -func (customError) Error() string { - return "custom error" -} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/test/common_test.go b/instrumentation/net/http/otelhttp/internal/semconv/test/common_test.go new file mode 100644 index 00000000000..5711c6c40a3 --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconv/test/common_test.go @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" + "go.opentelemetry.io/otel/attribute" +) + +type testServerReq struct { + hostname string + serverPort int + peerAddr string + peerPort int + clientIP string +} + +func testTraceRequest(t *testing.T, serv semconv.HTTPServer, want func(testServerReq) []attribute.KeyValue) { + t.Helper() + + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + close(got) + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := semconv.SplitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + srvReq := testServerReq{ + hostname: srvURL.Hostname(), + serverPort: int(srvPort), + peerAddr: peer, + peerPort: peerPort, + clientIP: clientIP, + } + + assert.ElementsMatch(t, want(srvReq), serv.RequestTraceAttrs("", req)) +} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/test/go.mod b/instrumentation/net/http/otelhttp/internal/semconv/test/go.mod new file mode 100644 index 00000000000..39fcd365602 --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconv/test/go.mod @@ -0,0 +1,25 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/test + +go 1.22 + +require ( + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + golang.org/x/sys v0.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => ../../../ diff --git a/instrumentation/net/http/otelhttp/internal/semconv/test/go.sum b/instrumentation/net/http/otelhttp/internal/semconv/test/go.sum new file mode 100644 index 00000000000..9362e90df3f --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconv/test/go.sum @@ -0,0 +1,31 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.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= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/net/http/otelhttp/internal/semconv/test/httpconv_test.go b/instrumentation/net/http/otelhttp/internal/semconv/test/httpconv_test.go new file mode 100644 index 00000000000..da37f9765b3 --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconv/test/httpconv_test.go @@ -0,0 +1,192 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" + "go.opentelemetry.io/otel/attribute" +) + +func TestNewTraceRequest(t *testing.T) { + t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") + serv := semconv.NewHTTPServer(nil) + want := func(req testServerReq) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("http.request.method", "GET"), + attribute.String("url.scheme", "http"), + attribute.String("server.address", req.hostname), + attribute.Int("server.port", req.serverPort), + attribute.String("network.peer.address", req.peerAddr), + attribute.Int("network.peer.port", req.peerPort), + attribute.String("user_agent.original", "Go-http-client/1.1"), + attribute.String("client.address", req.clientIP), + attribute.String("network.protocol.version", "1.1"), + attribute.String("url.path", "/"), + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("net.host.name", req.hostname), + attribute.Int("net.host.port", req.serverPort), + attribute.String("net.sock.peer.addr", req.peerAddr), + attribute.Int("net.sock.peer.port", req.peerPort), + attribute.String("user_agent.original", "Go-http-client/1.1"), + attribute.String("http.client_ip", req.clientIP), + attribute.String("net.protocol.version", "1.1"), + attribute.String("http.target", "/"), + } + } + testTraceRequest(t, serv, want) +} + +func TestNewTraceResponse(t *testing.T) { + testCases := []struct { + name string + resp semconv.ResponseTelemetry + want []attribute.KeyValue + }{ + { + name: "empty", + resp: semconv.ResponseTelemetry{}, + want: nil, + }, + { + name: "no errors", + resp: semconv.ResponseTelemetry{ + StatusCode: 200, + ReadBytes: 701, + WriteBytes: 802, + }, + want: []attribute.KeyValue{ + attribute.Int("http.request.body.size", 701), + attribute.Int("http.response.body.size", 802), + attribute.Int("http.response.status_code", 200), + }, + }, + { + name: "with errors", + resp: semconv.ResponseTelemetry{ + StatusCode: 200, + ReadBytes: 701, + ReadError: fmt.Errorf("read error"), + WriteBytes: 802, + WriteError: fmt.Errorf("write error"), + }, + want: []attribute.KeyValue{ + attribute.Int("http.request.body.size", 701), + attribute.Int("http.response.body.size", 802), + attribute.Int("http.response.status_code", 200), + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := semconv.CurrentHTTPServer{}.ResponseTraceAttrs(tt.resp) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestNewTraceRequest_Client(t *testing.T) { + t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") + body := strings.NewReader("Hello, world!") + url := "https://example.com:8888/foo/bar?stuff=morestuff" + req := httptest.NewRequest("pOST", url, body) + req.Header.Set("User-Agent", "go-test-agent") + + want := []attribute.KeyValue{ + attribute.String("http.request.method", "POST"), + attribute.String("http.request.method_original", "pOST"), + attribute.String("http.method", "pOST"), + attribute.String("url.full", url), + attribute.String("http.url", url), + attribute.String("server.address", "example.com"), + attribute.Int("server.port", 8888), + attribute.String("network.protocol.version", "1.1"), + attribute.String("net.peer.name", "example.com"), + attribute.Int("net.peer.port", 8888), + attribute.String("user_agent.original", "go-test-agent"), + attribute.Int("http.request_content_length", 13), + } + client := semconv.NewHTTPClient(nil) + assert.ElementsMatch(t, want, client.RequestTraceAttrs(req)) +} + +func TestNewTraceResponse_Client(t *testing.T) { + t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "http/dup") + testcases := []struct { + resp http.Response + want []attribute.KeyValue + }{ + {resp: http.Response{StatusCode: 200, ContentLength: 123}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 200), attribute.Int("http.status_code", 200), attribute.Int("http.response_content_length", 123)}}, + {resp: http.Response{StatusCode: 404, ContentLength: 0}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 404), attribute.Int("http.status_code", 404), attribute.String("error.type", "404")}}, + } + + for _, tt := range testcases { + client := semconv.NewHTTPClient(nil) + assert.ElementsMatch(t, tt.want, client.ResponseTraceAttrs(&tt.resp)) + } +} + +func TestClientRequest(t *testing.T) { + body := strings.NewReader("Hello, world!") + url := "https://example.com:8888/foo/bar?stuff=morestuff" + req := httptest.NewRequest("pOST", url, body) + req.Header.Set("User-Agent", "go-test-agent") + + want := []attribute.KeyValue{ + attribute.String("http.request.method", "POST"), + attribute.String("http.request.method_original", "pOST"), + attribute.String("url.full", url), + attribute.String("server.address", "example.com"), + attribute.Int("server.port", 8888), + attribute.String("network.protocol.version", "1.1"), + } + got := semconv.CurrentHTTPClient{}.RequestTraceAttrs(req) + assert.ElementsMatch(t, want, got) +} + +func TestClientResponse(t *testing.T) { + testcases := []struct { + resp http.Response + want []attribute.KeyValue + }{ + {resp: http.Response{StatusCode: 200, ContentLength: 123}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 200)}}, + {resp: http.Response{StatusCode: 404, ContentLength: 0}, want: []attribute.KeyValue{attribute.Int("http.response.status_code", 404), attribute.String("error.type", "404")}}, + } + + for _, tt := range testcases { + got := semconv.CurrentHTTPClient{}.ResponseTraceAttrs(&tt.resp) + assert.ElementsMatch(t, tt.want, got) + } +} + +func TestRequestErrorType(t *testing.T) { + testcases := []struct { + err error + want attribute.KeyValue + }{ + {err: errors.New("http: nil Request.URL"), want: attribute.String("error.type", "*errors.errorString")}, + {err: customError{}, want: attribute.String("error.type", "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/test.customError")}, + } + + for _, tt := range testcases { + got := semconv.CurrentHTTPClient{}.ErrorType(tt.err) + assert.Equal(t, tt.want, got) + } +} + +type customError struct{} + +func (customError) Error() string { + return "custom error" +} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/test/v1.20.0_test.go b/instrumentation/net/http/otelhttp/internal/semconv/test/v1.20.0_test.go new file mode 100644 index 00000000000..e46d7b2781f --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconv/test/v1.20.0_test.go @@ -0,0 +1,308 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" +) + +func TestV120TraceRequest(t *testing.T) { + // Anything but "http" or "http/dup" works. + t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "old") + serv := semconv.NewHTTPServer(nil) + want := func(req testServerReq) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("net.host.name", req.hostname), + attribute.Int("net.host.port", req.serverPort), + attribute.String("net.sock.peer.addr", req.peerAddr), + attribute.Int("net.sock.peer.port", req.peerPort), + attribute.String("user_agent.original", "Go-http-client/1.1"), + attribute.String("http.client_ip", req.clientIP), + attribute.String("net.protocol.version", "1.1"), + attribute.String("http.target", "/"), + } + } + testTraceRequest(t, serv, want) +} + +func TestV120TraceResponse(t *testing.T) { + testCases := []struct { + name string + resp semconv.ResponseTelemetry + want []attribute.KeyValue + }{ + { + name: "empty", + resp: semconv.ResponseTelemetry{}, + want: nil, + }, + { + name: "no errors", + resp: semconv.ResponseTelemetry{ + StatusCode: 200, + ReadBytes: 701, + WriteBytes: 802, + }, + want: []attribute.KeyValue{ + attribute.Int("http.request_content_length", 701), + attribute.Int("http.response_content_length", 802), + attribute.Int("http.status_code", 200), + }, + }, + { + name: "with errors", + resp: semconv.ResponseTelemetry{ + StatusCode: 200, + ReadBytes: 701, + ReadError: fmt.Errorf("read error"), + WriteBytes: 802, + WriteError: fmt.Errorf("write error"), + }, + want: []attribute.KeyValue{ + attribute.Int("http.request_content_length", 701), + attribute.String("http.read_error", "read error"), + attribute.Int("http.response_content_length", 802), + attribute.String("http.write_error", "write error"), + attribute.Int("http.status_code", 200), + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := semconv.OldHTTPServer{}.ResponseTraceAttrs(tt.resp) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestV120RecordMetrics(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + + server := semconv.NewHTTPServer(mp.Meter("test")) + req, err := http.NewRequest("POST", "http://example.com", nil) + assert.NoError(t, err) + + server.RecordMetrics(context.Background(), semconv.ServerMetricData{ + ServerName: "stuff", + ResponseSize: 200, + MetricAttributes: semconv.MetricAttributes{ + Req: req, + StatusCode: 301, + AdditionalAttributes: []attribute.KeyValue{ + attribute.String("key", "value"), + }, + }, + MetricData: semconv.MetricData{ + RequestSize: 100, + ElapsedTime: 300, + }, + }) + + rm := metricdata.ResourceMetrics{} + require.NoError(t, reader.Collect(context.Background(), &rm)) + require.Len(t, rm.ScopeMetrics, 1) + require.Len(t, rm.ScopeMetrics[0].Metrics, 3) + + attrs := attribute.NewSet( + attribute.String("http.scheme", "http"), + attribute.String("http.method", "POST"), + attribute.Int64("http.status_code", 301), + attribute.String("key", "value"), + attribute.String("net.host.name", "stuff"), + attribute.String("net.protocol.name", "http"), + attribute.String("net.protocol.version", "1.1"), + ) + + expectedScopeMetric := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "test", + }, + Metrics: []metricdata.Metrics{ + { + Name: "http.server.request.size", + Description: "Measures the size of HTTP request messages.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attrs, + }, + }, + }, + }, + { + Name: "http.server.response.size", + Description: "Measures the size of HTTP response messages.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attrs, + }, + }, + }, + }, + { + Name: "http.server.duration", + Description: "Measures the duration of inbound HTTP requests.", + Unit: "ms", + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attrs, + }, + }, + }, + }, + }, + } + + metricdatatest.AssertEqual(t, expectedScopeMetric, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) +} + +func TestV120ClientRequest(t *testing.T) { + body := strings.NewReader("Hello, world!") + url := "https://example.com:8888/foo/bar?stuff=morestuff" + req, err := http.NewRequest("POST", url, body) + assert.NoError(t, err) + req.Header.Set("User-Agent", "go-test-agent") + + want := []attribute.KeyValue{ + attribute.String("http.method", "POST"), + attribute.String("http.url", url), + attribute.String("net.peer.name", "example.com"), + attribute.Int("net.peer.port", 8888), + attribute.Int("http.request_content_length", body.Len()), + attribute.String("user_agent.original", "go-test-agent"), + } + got := semconv.OldHTTPClient{}.RequestTraceAttrs(req) + assert.ElementsMatch(t, want, got) +} + +func TestV120ClientResponse(t *testing.T) { + resp := http.Response{ + StatusCode: 200, + ContentLength: 123, + } + + want := []attribute.KeyValue{ + attribute.Int("http.response_content_length", 123), + attribute.Int("http.status_code", 200), + } + + got := semconv.OldHTTPClient{}.ResponseTraceAttrs(&resp) + assert.ElementsMatch(t, want, got) +} + +func TestV120ClientMetrics(t *testing.T) { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + + client := semconv.NewHTTPClient(mp.Meter("test")) + req, err := http.NewRequest("POST", "http://example.com", nil) + assert.NoError(t, err) + + opts := client.MetricOptions(semconv.MetricAttributes{ + Req: req, + StatusCode: 301, + AdditionalAttributes: []attribute.KeyValue{ + attribute.String("key", "value"), + }, + }) + + ctx := context.Background() + + client.RecordResponseSize(ctx, 200, opts.AddOptions()) + + client.RecordMetrics(ctx, semconv.MetricData{ + RequestSize: 100, + ElapsedTime: 300, + }, opts) + + rm := metricdata.ResourceMetrics{} + require.NoError(t, reader.Collect(context.Background(), &rm)) + require.Len(t, rm.ScopeMetrics, 1) + require.Len(t, rm.ScopeMetrics[0].Metrics, 3) + + attrs := attribute.NewSet( + attribute.String("http.method", "POST"), + attribute.Int64("http.status_code", 301), + attribute.String("key", "value"), + attribute.String("net.peer.name", "example.com"), + ) + + expectedScopeMetric := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "test", + }, + Metrics: []metricdata.Metrics{ + { + Name: "http.client.request.size", + Description: "Measures the size of HTTP request messages.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attrs, + }, + }, + }, + }, + { + Name: "http.client.response.size", + Description: "Measures the size of HTTP response messages.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attrs, + }, + }, + }, + }, + { + Name: "http.client.duration", + Description: "Measures the duration of outbound HTTP requests.", + Unit: "ms", + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attrs, + }, + }, + }, + }, + }, + } + + metricdatatest.AssertEqual(t, expectedScopeMetric, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) +} diff --git a/instrumentation/net/http/otelhttp/internal/semconv/util.go b/instrumentation/net/http/otelhttp/internal/semconv/util.go index e6e14924f57..93e8d0f94c1 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/util.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/util.go @@ -14,14 +14,14 @@ import ( semconvNew "go.opentelemetry.io/otel/semconv/v1.26.0" ) -// splitHostPort splits a network address hostport of the form "host", +// SplitHostPort splits a network address hostport of the form "host", // "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", // "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and // port. // // An empty host is returned if it is not provided or unparsable. A negative // port is returned if it is not provided or unparsable. -func splitHostPort(hostport string) (host string, port int) { +func SplitHostPort(hostport string) (host string, port int) { port = -1 if strings.HasPrefix(hostport, "[") { diff --git a/instrumentation/net/http/otelhttp/internal/semconv/util_test.go b/instrumentation/net/http/otelhttp/internal/semconv/util_test.go index c5310f90d80..b0fe5439883 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/util_test.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/util_test.go @@ -34,7 +34,7 @@ func TestSplitHostPort(t *testing.T) { } for _, test := range tests { - h, p := splitHostPort(test.hostport) + h, p := SplitHostPort(test.hostport) assert.Equal(t, test.host, h, test.hostport) assert.Equal(t, test.port, p, test.hostport) } diff --git a/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go b/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go index 5367732ec5d..c042249dd72 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0.go @@ -17,7 +17,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.20.0" ) -type oldHTTPServer struct{} +type OldHTTPServer struct{} // RequestTraceAttrs returns trace attributes for an HTTP request received by a // server. @@ -35,14 +35,14 @@ type oldHTTPServer struct{} // // If the primary server name is not known, server should be an empty string. // The req Host will be used to determine the server instead. -func (o oldHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { +func (o OldHTTPServer) RequestTraceAttrs(server string, req *http.Request) []attribute.KeyValue { return semconvutil.HTTPServerRequest(server, req) } // ResponseTraceAttrs returns trace attributes for telemetry from an HTTP response. // // If any of the fields in the ResponseTelemetry are not set the attribute will be omitted. -func (o oldHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { +func (o OldHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { attributes := []attribute.KeyValue{} if resp.ReadBytes > 0 { @@ -67,7 +67,7 @@ func (o oldHTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.Ke } // Route returns the attribute for the route. -func (o oldHTTPServer) Route(route string) attribute.KeyValue { +func (o OldHTTPServer) Route(route string) attribute.KeyValue { return semconv.HTTPRoute(route) } @@ -84,7 +84,7 @@ const ( serverDuration = "http.server.duration" // Incoming end to end duration, milliseconds ) -func (h oldHTTPServer) createMeasures(meter metric.Meter) (metric.Int64Counter, metric.Int64Counter, metric.Float64Histogram) { +func (h OldHTTPServer) createMeasures(meter metric.Meter) (metric.Int64Counter, metric.Int64Counter, metric.Float64Histogram) { if meter == nil { return noop.Int64Counter{}, noop.Int64Counter{}, noop.Float64Histogram{} } @@ -113,17 +113,17 @@ func (h oldHTTPServer) createMeasures(meter metric.Meter) (metric.Int64Counter, return requestBytesCounter, responseBytesCounter, serverLatencyMeasure } -func (o oldHTTPServer) MetricAttributes(server string, req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { +func (o OldHTTPServer) MetricAttributes(server string, req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { n := len(additionalAttributes) + 3 var host string var p int if server == "" { - host, p = splitHostPort(req.Host) + host, p = SplitHostPort(req.Host) } else { // Prioritize the primary server name. - host, p = splitHostPort(server) + host, p = SplitHostPort(server) if p < 0 { - _, p = splitHostPort(req.Host) + _, p = SplitHostPort(req.Host) } } hostPort := requiredHTTPPort(req.TLS != nil, p) @@ -164,24 +164,24 @@ func (o oldHTTPServer) MetricAttributes(server string, req *http.Request, status return attributes } -func (o oldHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive +func (o OldHTTPServer) scheme(https bool) attribute.KeyValue { // nolint:revive if https { return semconv.HTTPSchemeHTTPS } return semconv.HTTPSchemeHTTP } -type oldHTTPClient struct{} +type OldHTTPClient struct{} -func (o oldHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { +func (o OldHTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { return semconvutil.HTTPClientRequest(req) } -func (o oldHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { +func (o OldHTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { return semconvutil.HTTPClientResponse(resp) } -func (o oldHTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { +func (o OldHTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { /* The following semantic conventions are returned if present: http.method string http.status_code int @@ -197,7 +197,7 @@ func (o oldHTTPClient) MetricAttributes(req *http.Request, statusCode int, addit var requestHost string var requestPort int for _, hostport := range []string{h, req.Header.Get("Host")} { - requestHost, requestPort = splitHostPort(hostport) + requestHost, requestPort = SplitHostPort(hostport) if requestHost != "" || requestPort > 0 { break } @@ -235,7 +235,7 @@ const ( clientDuration = "http.client.duration" // Incoming end to end duration, milliseconds ) -func (o oldHTTPClient) createMeasures(meter metric.Meter) (metric.Int64Counter, metric.Int64Counter, metric.Float64Histogram) { +func (o OldHTTPClient) createMeasures(meter metric.Meter) (metric.Int64Counter, metric.Int64Counter, metric.Float64Histogram) { if meter == nil { return noop.Int64Counter{}, noop.Int64Counter{}, noop.Float64Histogram{} } diff --git a/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0_test.go b/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0_test.go index eef382a5047..a3731492729 100644 --- a/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0_test.go +++ b/instrumentation/net/http/otelhttp/internal/semconv/v1.20.0_test.go @@ -4,10 +4,6 @@ package semconv import ( - "context" - "fmt" - "net/http" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,190 +11,6 @@ import ( "go.opentelemetry.io/otel/attribute" ) -func TestV120TraceRequest(t *testing.T) { - // Anything but "http" or "http/dup" works. - t.Setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "old") - serv := NewHTTPServer(nil) - want := func(req testServerReq) []attribute.KeyValue { - return []attribute.KeyValue{ - attribute.String("http.method", "GET"), - attribute.String("http.scheme", "http"), - attribute.String("net.host.name", req.hostname), - attribute.Int("net.host.port", req.serverPort), - attribute.String("net.sock.peer.addr", req.peerAddr), - attribute.Int("net.sock.peer.port", req.peerPort), - attribute.String("user_agent.original", "Go-http-client/1.1"), - attribute.String("http.client_ip", req.clientIP), - attribute.String("net.protocol.version", "1.1"), - attribute.String("http.target", "/"), - } - } - testTraceRequest(t, serv, want) -} - -func TestV120TraceResponse(t *testing.T) { - testCases := []struct { - name string - resp ResponseTelemetry - want []attribute.KeyValue - }{ - { - name: "empty", - resp: ResponseTelemetry{}, - want: nil, - }, - { - name: "no errors", - resp: ResponseTelemetry{ - StatusCode: 200, - ReadBytes: 701, - WriteBytes: 802, - }, - want: []attribute.KeyValue{ - attribute.Int("http.request_content_length", 701), - attribute.Int("http.response_content_length", 802), - attribute.Int("http.status_code", 200), - }, - }, - { - name: "with errors", - resp: ResponseTelemetry{ - StatusCode: 200, - ReadBytes: 701, - ReadError: fmt.Errorf("read error"), - WriteBytes: 802, - WriteError: fmt.Errorf("write error"), - }, - want: []attribute.KeyValue{ - attribute.Int("http.request_content_length", 701), - attribute.String("http.read_error", "read error"), - attribute.Int("http.response_content_length", 802), - attribute.String("http.write_error", "write error"), - attribute.Int("http.status_code", 200), - }, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - got := oldHTTPServer{}.ResponseTraceAttrs(tt.resp) - assert.ElementsMatch(t, tt.want, got) - }) - } -} - -func TestV120RecordMetrics(t *testing.T) { - server := NewTestHTTPServer() - req, err := http.NewRequest("POST", "http://example.com", nil) - assert.NoError(t, err) - - server.RecordMetrics(context.Background(), ServerMetricData{ - ServerName: "stuff", - ResponseSize: 200, - MetricAttributes: MetricAttributes{ - Req: req, - StatusCode: 301, - AdditionalAttributes: []attribute.KeyValue{ - attribute.String("key", "value"), - }, - }, - MetricData: MetricData{ - RequestSize: 100, - ElapsedTime: 300, - }, - }) - - assert.Equal(t, int64(100), server.requestBytesCounter.(*testInst).intValue) - assert.Equal(t, int64(200), server.responseBytesCounter.(*testInst).intValue) - assert.Equal(t, float64(300), server.serverLatencyMeasure.(*testInst).floatValue) - - want := []attribute.KeyValue{ - attribute.String("http.scheme", "http"), - attribute.String("http.method", "POST"), - attribute.Int64("http.status_code", 301), - attribute.String("key", "value"), - attribute.String("net.host.name", "stuff"), - attribute.String("net.protocol.name", "http"), - attribute.String("net.protocol.version", "1.1"), - } - - assert.ElementsMatch(t, want, server.requestBytesCounter.(*testInst).attributes) - assert.ElementsMatch(t, want, server.responseBytesCounter.(*testInst).attributes) - assert.ElementsMatch(t, want, server.serverLatencyMeasure.(*testInst).attributes) -} - -func TestV120ClientRequest(t *testing.T) { - body := strings.NewReader("Hello, world!") - url := "https://example.com:8888/foo/bar?stuff=morestuff" - req, err := http.NewRequest("POST", url, body) - assert.NoError(t, err) - req.Header.Set("User-Agent", "go-test-agent") - - want := []attribute.KeyValue{ - attribute.String("http.method", "POST"), - attribute.String("http.url", url), - attribute.String("net.peer.name", "example.com"), - attribute.Int("net.peer.port", 8888), - attribute.Int("http.request_content_length", body.Len()), - attribute.String("user_agent.original", "go-test-agent"), - } - got := oldHTTPClient{}.RequestTraceAttrs(req) - assert.ElementsMatch(t, want, got) -} - -func TestV120ClientResponse(t *testing.T) { - resp := http.Response{ - StatusCode: 200, - ContentLength: 123, - } - - want := []attribute.KeyValue{ - attribute.Int("http.response_content_length", 123), - attribute.Int("http.status_code", 200), - } - - got := oldHTTPClient{}.ResponseTraceAttrs(&resp) - assert.ElementsMatch(t, want, got) -} - -func TestV120ClientMetrics(t *testing.T) { - client := NewTestHTTPClient() - req, err := http.NewRequest("POST", "http://example.com", nil) - assert.NoError(t, err) - - opts := client.MetricOptions(MetricAttributes{ - Req: req, - StatusCode: 301, - AdditionalAttributes: []attribute.KeyValue{ - attribute.String("key", "value"), - }, - }) - - ctx := context.Background() - - client.RecordResponseSize(ctx, 200, opts.AddOptions()) - - client.RecordMetrics(ctx, MetricData{ - RequestSize: 100, - ElapsedTime: 300, - }, opts) - - assert.Equal(t, int64(100), client.requestBytesCounter.(*testInst).intValue) - assert.Equal(t, int64(200), client.responseBytesCounter.(*testInst).intValue) - assert.Equal(t, float64(300), client.latencyMeasure.(*testInst).floatValue) - - want := []attribute.KeyValue{ - attribute.String("http.method", "POST"), - attribute.Int64("http.status_code", 301), - attribute.String("key", "value"), - attribute.String("net.peer.name", "example.com"), - } - - assert.ElementsMatch(t, want, client.requestBytesCounter.(*testInst).attributes) - assert.ElementsMatch(t, want, client.responseBytesCounter.(*testInst).attributes) - assert.ElementsMatch(t, want, client.latencyMeasure.(*testInst).attributes) -} - func TestStandardizeHTTPMethodMetric(t *testing.T) { testCases := []struct { method string