diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eab6b9e4b..1ae1320ff 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -405,6 +405,16 @@ updates: schedule: interval: "weekly" day: "sunday" + - + package-ecosystem: "gomod" + directory: "/instrumentation/net/http/otelhttp/test" + labels: + - dependencies + - go + - "Skip Changelog" + schedule: + interval: "weekly" + day: "sunday" - package-ecosystem: "gomod" directory: "/instrumentation/net/http/httptrace/otelhttptrace" diff --git a/instrumentation/net/http/otelhttp/example/go.sum b/instrumentation/net/http/otelhttp/example/go.sum index 5c95769b0..4cedbf3a9 100644 --- a/instrumentation/net/http/otelhttp/example/go.sum +++ b/instrumentation/net/http/otelhttp/example/go.sum @@ -19,8 +19,6 @@ go.opentelemetry.io/otel/internal/metric v0.22.0/go.mod h1:7qVuMihW/ktMonEfOvBXu go.opentelemetry.io/otel/metric v0.22.0 h1:/qv10BzznqEifrXBwsTT370OCN1PRgt+mnjzMwxJKrQ= go.opentelemetry.io/otel/metric v0.22.0/go.mod h1:KcsUkBiYGW003DJ+ugd2aqIRIfjabD9jeOUXqsAtrq0= go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1Gb4mL/9lpDkZ0JjMRq4= -go.opentelemetry.io/otel/oteltest v1.0.0-RC2 h1:xNKqMhlZYkASSyvF4JwObZFMq0jhFN3c3SP+2rCzVPk= -go.opentelemetry.io/otel/oteltest v1.0.0-RC2/go.mod h1:kiQ4tw5tAL4JLTbcOYwK1CWI1HkT5aiLzHovgOVnz/A= go.opentelemetry.io/otel/sdk v1.0.0-RC2 h1:ROuteeSCBaZNjiT9JcFzZepmInDvLktR28Y6qKo8bCs= go.opentelemetry.io/otel/sdk v1.0.0-RC2/go.mod h1:fgwHyiDn4e5k40TD9VX243rOxXR+jzsWBZYA2P5jpEw= go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= diff --git a/instrumentation/net/http/otelhttp/go.mod b/instrumentation/net/http/otelhttp/go.mod index 53816c8bb..16c5c56b3 100644 --- a/instrumentation/net/http/otelhttp/go.mod +++ b/instrumentation/net/http/otelhttp/go.mod @@ -10,6 +10,5 @@ require ( go.opentelemetry.io/contrib v0.22.0 go.opentelemetry.io/otel v1.0.0-RC2 go.opentelemetry.io/otel/metric v0.22.0 - go.opentelemetry.io/otel/oteltest v1.0.0-RC2 go.opentelemetry.io/otel/trace v1.0.0-RC2 ) diff --git a/instrumentation/net/http/otelhttp/go.sum b/instrumentation/net/http/otelhttp/go.sum index eb920f755..db1077fed 100644 --- a/instrumentation/net/http/otelhttp/go.sum +++ b/instrumentation/net/http/otelhttp/go.sum @@ -17,8 +17,6 @@ go.opentelemetry.io/otel/internal/metric v0.22.0/go.mod h1:7qVuMihW/ktMonEfOvBXu go.opentelemetry.io/otel/metric v0.22.0 h1:/qv10BzznqEifrXBwsTT370OCN1PRgt+mnjzMwxJKrQ= go.opentelemetry.io/otel/metric v0.22.0/go.mod h1:KcsUkBiYGW003DJ+ugd2aqIRIfjabD9jeOUXqsAtrq0= go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1Gb4mL/9lpDkZ0JjMRq4= -go.opentelemetry.io/otel/oteltest v1.0.0-RC2 h1:xNKqMhlZYkASSyvF4JwObZFMq0jhFN3c3SP+2rCzVPk= -go.opentelemetry.io/otel/oteltest v1.0.0-RC2/go.mod h1:kiQ4tw5tAL4JLTbcOYwK1CWI1HkT5aiLzHovgOVnz/A= go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= go.opentelemetry.io/otel/trace v1.0.0-RC2 h1:dunAP0qDULMIT82atj34m5RgvsIK6LcsXf1c/MsYg1w= go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4= diff --git a/instrumentation/net/http/otelhttp/handler_test.go b/instrumentation/net/http/otelhttp/handler_test.go index c718b0a36..481eb526d 100644 --- a/instrumentation/net/http/otelhttp/handler_test.go +++ b/instrumentation/net/http/otelhttp/handler_test.go @@ -11,195 +11,51 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package otelhttp + +package otelhttp_test import ( - "fmt" - "io" "io/ioutil" "net/http" "net/http/httptest" - "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric/metrictest" - "go.opentelemetry.io/otel/oteltest" - "go.opentelemetry.io/otel/propagation" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -func assertMetricAttributes(t *testing.T, expectedAttributes []attribute.KeyValue, measurementBatches []metrictest.Batch) { - for _, batch := range measurementBatches { - assert.ElementsMatch(t, expectedAttributes, batch.Labels) - } -} - -func TestHandlerBasics(t *testing.T) { - rr := httptest.NewRecorder() - - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) - meterimpl, meterProvider := metrictest.NewMeterProvider() - - operation := "test_handler" - - h := NewHandler( +func TestResponseWriterImplementsFlusher(t *testing.T) { + h := otelhttp.NewHandler( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - l, _ := LabelerFromContext(r.Context()) - l.Add(attribute.String("test", "attribute")) - - if _, err := io.WriteString(w, "hello world"); err != nil { - t.Fatal(err) - } - }), operation, - WithTracerProvider(provider), - WithMeterProvider(meterProvider), - WithPropagators(propagation.TraceContext{}), - ) - - r, err := http.NewRequest(http.MethodGet, "http://localhost/", strings.NewReader("foo")) - if err != nil { - t.Fatal(err) - } - h.ServeHTTP(rr, r) - - if len(meterimpl.MeasurementBatches) == 0 { - t.Fatalf("got 0 recorded measurements, expected 1 or more") - } - - attributesToVerify := []attribute.KeyValue{ - semconv.HTTPServerNameKey.String(operation), - semconv.HTTPSchemeHTTP, - semconv.HTTPHostKey.String(r.Host), - semconv.HTTPFlavorKey.String(fmt.Sprintf("1.%d", r.ProtoMinor)), - attribute.String("test", "attribute"), - } - - assertMetricAttributes(t, attributesToVerify, meterimpl.MeasurementBatches) - - if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected { - t.Fatalf("got %d, expected %d", got, expected) - } - if got := rr.Header().Get("Traceparent"); got == "" { - t.Fatal("expected non empty trace header") - } - - spans := spanRecorder.Completed() - if got, expected := len(spans), 1; got != expected { - t.Fatalf("got %d spans, expected %d", got, expected) - } - expectSpanID := trace.SpanID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2} // we expect the span ID to be incremented by one - if got, expected := spans[0].SpanContext().SpanID(), expectSpanID; got != expected { - t.Fatalf("got %d, expected %d", got, expected) - } - - d, err := ioutil.ReadAll(rr.Result().Body) - if err != nil { - t.Fatal(err) - } - if got, expected := string(d), "hello world"; got != expected { - t.Fatalf("got %q, expected %q", got, expected) - } -} - -func TestHandlerNoWrite(t *testing.T) { - rr := httptest.NewRecorder() - provider := oteltest.NewTracerProvider() - - operation := "test_handler" - var span trace.Span - - h := NewHandler( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - span = trace.SpanFromContext(r.Context()) - }), operation, - WithTracerProvider(provider), - WithPropagators(propagation.TraceContext{}), - ) - - r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil) - if err != nil { - t.Fatal(err) - } - h.ServeHTTP(rr, r) - - if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected { - t.Fatalf("got %d, expected %d", got, expected) - } - if got := rr.Header().Get("Traceparent"); got != "" { - t.Fatal("expected empty trace header") - } - expectSpanID := trace.SpanID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2} // we expect the span ID to be incremented by one - if got, expected := span.SpanContext().SpanID(), expectSpanID; got != expected { - t.Fatalf("got %d, expected %d", got, expected) - } - if mockSpan, ok := span.(*oteltest.Span); ok { - if got, expected := mockSpan.StatusCode(), codes.Unset; got != expected { - t.Fatalf("got %q, expected %q", got, expected) - } - } else { - t.Fatalf("Expected *moctrace.MockSpan, got %T", span) - } -} - -func TestResponseWriterOptionalInterfaces(t *testing.T) { - rr := httptest.NewRecorder() - - provider := oteltest.NewTracerProvider() - - // ResponseRecorder implements the Flusher interface. Make sure the - // wrapped ResponseWriter passed to the handler still implements - // Flusher. - - var isFlusher bool - h := NewHandler( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, isFlusher = w.(http.Flusher) - if _, err := io.WriteString(w, "hello world"); err != nil { - t.Fatal(err) - } + assert.Implements(t, (*http.Flusher)(nil), w) }), "test_handler", - WithTracerProvider(provider)) + ) r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil) - if err != nil { - t.Fatal(err) - } - h.ServeHTTP(rr, r) - if !isFlusher { - t.Fatal("http.Flusher interface not exposed") - } + require.NoError(t, err) + + h.ServeHTTP(httptest.NewRecorder(), r) } // This use case is important as we make sure the body isn't mutated // when it is nil. This is a common use case for tests where the request // is directly passed to the handler. func TestHandlerReadingNilBodySuccess(t *testing.T) { - rr := httptest.NewRecorder() - - provider := oteltest.NewTracerProvider() - - h := NewHandler( + h := otelhttp.NewHandler( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body != nil { _, err := ioutil.ReadAll(r.Body) - assert.NotNil(t, err) + assert.NoError(t, err) } }), "test_handler", - WithTracerProvider(provider), ) r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + rr := httptest.NewRecorder() h.ServeHTTP(rr, r) assert.Equal(t, 200, rr.Result().StatusCode) } diff --git a/instrumentation/net/http/otelhttp/client_test.go b/instrumentation/net/http/otelhttp/test/client_test.go similarity index 69% rename from instrumentation/net/http/otelhttp/client_test.go rename to instrumentation/net/http/otelhttp/test/client_test.go index 800c64513..aeb732ab9 100644 --- a/instrumentation/net/http/otelhttp/client_test.go +++ b/instrumentation/net/http/otelhttp/test/client_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package otelhttp +package test import ( "context" @@ -25,20 +25,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func TestConvenienceWrappers(t *testing.T) { - sr := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider(oteltest.WithSpanRecorder(sr)) - orig := DefaultClient - DefaultClient = &http.Client{ - Transport: NewTransport( + sr := tracetest.NewSpanRecorder() + provider := trace.NewTracerProvider(trace.WithSpanProcessor(sr)) + orig := otelhttp.DefaultClient + otelhttp.DefaultClient = &http.Client{ + Transport: otelhttp.NewTransport( http.DefaultTransport, - WithTracerProvider(provider), + otelhttp.WithTracerProvider(provider), ), } - defer func() { DefaultClient = orig }() + defer func() { otelhttp.DefaultClient = orig }() content := []byte("Hello, world!") @@ -50,19 +52,19 @@ func TestConvenienceWrappers(t *testing.T) { defer ts.Close() ctx := context.Background() - res, err := Get(ctx, ts.URL) + res, err := otelhttp.Get(ctx, ts.URL) if err != nil { t.Fatal(err) } res.Body.Close() - res, err = Head(ctx, ts.URL) + res, err = otelhttp.Head(ctx, ts.URL) if err != nil { t.Fatal(err) } res.Body.Close() - res, err = Post(ctx, ts.URL, "text/plain", strings.NewReader("test")) + res, err = otelhttp.Post(ctx, ts.URL, "text/plain", strings.NewReader("test")) if err != nil { t.Fatal(err) } @@ -70,13 +72,13 @@ func TestConvenienceWrappers(t *testing.T) { form := make(url.Values) form.Set("foo", "bar") - res, err = PostForm(ctx, ts.URL, form) + res, err = otelhttp.PostForm(ctx, ts.URL, form) if err != nil { t.Fatal(err) } res.Body.Close() - spans := sr.Completed() + spans := sr.Ended() require.Equal(t, 4, len(spans)) assert.Equal(t, "HTTP GET", spans[0].Name()) assert.Equal(t, "HTTP HEAD", spans[1].Name()) diff --git a/instrumentation/net/http/otelhttp/config_test.go b/instrumentation/net/http/otelhttp/test/config_test.go similarity index 72% rename from instrumentation/net/http/otelhttp/config_test.go rename to instrumentation/net/http/otelhttp/test/config_test.go index 56939ff9f..a0727b6d0 100644 --- a/instrumentation/net/http/otelhttp/config_test.go +++ b/instrumentation/net/http/otelhttp/test/config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package otelhttp +package test import ( "io" @@ -23,25 +23,25 @@ import ( "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func TestBasicFilter(t *testing.T) { rr := httptest.NewRecorder() - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) + spanRecorder := tracetest.NewSpanRecorder() + provider := trace.NewTracerProvider(trace.WithSpanProcessor(spanRecorder)) - h := NewHandler( + h := otelhttp.NewHandler( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, err := io.WriteString(w, "hello world"); err != nil { t.Fatal(err) } }), "test_handler", - WithTracerProvider(provider), - WithFilter(func(r *http.Request) bool { + otelhttp.WithTracerProvider(provider), + otelhttp.WithFilter(func(r *http.Request) bool { return false }), ) @@ -57,7 +57,7 @@ func TestBasicFilter(t *testing.T) { if got := rr.Header().Get("Traceparent"); got != "" { t.Fatal("expected empty trace header") } - if got, expected := len(spanRecorder.Completed()), 0; got != expected { + if got, expected := len(spanRecorder.Ended()), 0; got != expected { t.Fatalf("got %d recorded spans, expected %d", got, expected) } d, err := ioutil.ReadAll(rr.Result().Body) @@ -77,15 +77,19 @@ func TestSpanNameFormatter(t *testing.T) { expected string }{ { - name: "default handler formatter", - formatter: defaultHandlerFormatter, + name: "default handler formatter", + formatter: func(operation string, _ *http.Request) string { + return operation + }, operation: "test_operation", expected: "test_operation", }, { - name: "default transport formatter", - formatter: defaultTransportFormatter, - expected: "HTTP GET", + name: "default transport formatter", + formatter: func(_ string, r *http.Request) string { + return "HTTP " + r.Method + }, + expected: "HTTP GET", }, { name: "custom formatter", @@ -101,20 +105,18 @@ func TestSpanNameFormatter(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) + spanRecorder := tracetest.NewSpanRecorder() + provider := trace.NewTracerProvider(trace.WithSpanProcessor(spanRecorder)) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, err := io.WriteString(w, "hello world"); err != nil { t.Fatal(err) } }) - h := NewHandler( + h := otelhttp.NewHandler( handler, tc.operation, - WithTracerProvider(provider), - WithSpanNameFormatter(tc.formatter), + otelhttp.WithTracerProvider(provider), + otelhttp.WithSpanNameFormatter(tc.formatter), ) r, err := http.NewRequest(http.MethodGet, "http://localhost/hello", nil) if err != nil { @@ -125,7 +127,7 @@ func TestSpanNameFormatter(t *testing.T) { t.Fatalf("got %d, expected %d", got, expected) } - spans := spanRecorder.Completed() + spans := spanRecorder.Ended() if assert.Len(t, spans, 1) { assert.Equal(t, tc.expected, spans[0].Name()) } diff --git a/instrumentation/net/http/otelhttp/test/doc.go b/instrumentation/net/http/otelhttp/test/doc.go new file mode 100644 index 000000000..0d190a314 --- /dev/null +++ b/instrumentation/net/http/otelhttp/test/doc.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package test validates the otelhttp instrumentation with the default SDK. + +This package is in a separate module from the instrumentation it tests to +isolate the dependency of the default SDK and not impose this as a transitive +dependency for users. +*/ +package test diff --git a/instrumentation/net/http/otelhttp/test/go.mod b/instrumentation/net/http/otelhttp/test/go.mod new file mode 100644 index 000000000..204c8bc93 --- /dev/null +++ b/instrumentation/net/http/otelhttp/test/go.mod @@ -0,0 +1,14 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/test + +go 1.15 + +require ( + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.22.0 + go.opentelemetry.io/otel v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b + go.opentelemetry.io/otel/metric v0.22.0 + go.opentelemetry.io/otel/sdk v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b + go.opentelemetry.io/otel/trace v1.0.0-RC2 +) + +replace go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.22.0 => ../ diff --git a/instrumentation/net/http/otelhttp/test/go.sum b/instrumentation/net/http/otelhttp/test/go.sum new file mode 100644 index 000000000..5836ed6fc --- /dev/null +++ b/instrumentation/net/http/otelhttp/test/go.sum @@ -0,0 +1,35 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/contrib v0.22.0 h1:0F7gDEjgb1WGn4ODIjaCAg75hmqF+UN0LiVgwxsCodc= +go.opentelemetry.io/contrib v0.22.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= +go.opentelemetry.io/otel v1.0.0-RC1/go.mod h1:x9tRa9HK4hSSq7jf2TKbqFbtt58/TGk0f9XiEYISI1I= +go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM= +go.opentelemetry.io/otel v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b h1:mVdpWpFdeOeGPCpwO95rocgtrkE12gZhDU4LA9K9TNE= +go.opentelemetry.io/otel v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b/go.mod h1:WrhiZahmIBdsXGO6mYjS6eW6kZzI/9GfGHFpRi8X/Yg= +go.opentelemetry.io/otel/internal/metric v0.22.0 h1:Q9bS02XRykSRIbggaU4hVF9oWOP9PyILu26zJWoKmk0= +go.opentelemetry.io/otel/internal/metric v0.22.0/go.mod h1:7qVuMihW/ktMonEfOvBXuh6tfMvvEyoIDgeJNRloYbQ= +go.opentelemetry.io/otel/metric v0.22.0 h1:/qv10BzznqEifrXBwsTT370OCN1PRgt+mnjzMwxJKrQ= +go.opentelemetry.io/otel/metric v0.22.0/go.mod h1:KcsUkBiYGW003DJ+ugd2aqIRIfjabD9jeOUXqsAtrq0= +go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1Gb4mL/9lpDkZ0JjMRq4= +go.opentelemetry.io/otel/sdk v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b h1:3L//VzNirHuL0jZSmHFeQOIdGvNmSsfnl4g9UV6ZRcI= +go.opentelemetry.io/otel/sdk v1.0.0-RC2.0.20210812161231-a8bb0bf89f3b/go.mod h1:RiCEArosW4fWBJshjrl1H4IAzoRwI0sIqfqac5ramT8= +go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= +go.opentelemetry.io/otel/trace v1.0.0-RC2 h1:dunAP0qDULMIT82atj34m5RgvsIK6LcsXf1c/MsYg1w= +go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/net/http/otelhttp/test/handler_test.go b/instrumentation/net/http/otelhttp/test/handler_test.go new file mode 100644 index 000000000..2d68a73a0 --- /dev/null +++ b/instrumentation/net/http/otelhttp/test/handler_test.go @@ -0,0 +1,109 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric/metrictest" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +func assertMetricAttributes(t *testing.T, expectedAttributes []attribute.KeyValue, measurementBatches []metrictest.Batch) { + for _, batch := range measurementBatches { + assert.ElementsMatch(t, expectedAttributes, batch.Labels) + } +} + +func TestHandlerBasics(t *testing.T) { + rr := httptest.NewRecorder() + + spanRecorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + + meterimpl, meterProvider := metrictest.NewMeterProvider() + + operation := "test_handler" + + h := otelhttp.NewHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l, _ := otelhttp.LabelerFromContext(r.Context()) + l.Add(attribute.String("test", "attribute")) + + if _, err := io.WriteString(w, "hello world"); err != nil { + t.Fatal(err) + } + }), operation, + otelhttp.WithTracerProvider(provider), + otelhttp.WithMeterProvider(meterProvider), + otelhttp.WithPropagators(propagation.TraceContext{}), + ) + + r, err := http.NewRequest(http.MethodGet, "http://localhost/", strings.NewReader("foo")) + if err != nil { + t.Fatal(err) + } + h.ServeHTTP(rr, r) + + if len(meterimpl.MeasurementBatches) == 0 { + t.Fatalf("got 0 recorded measurements, expected 1 or more") + } + + attributesToVerify := []attribute.KeyValue{ + semconv.HTTPServerNameKey.String(operation), + semconv.HTTPSchemeHTTP, + semconv.HTTPHostKey.String(r.Host), + semconv.HTTPFlavorKey.String(fmt.Sprintf("1.%d", r.ProtoMinor)), + attribute.String("test", "attribute"), + } + + assertMetricAttributes(t, attributesToVerify, meterimpl.MeasurementBatches) + + if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected { + t.Fatalf("got %d, expected %d", got, expected) + } + if got := rr.Header().Get("Traceparent"); got == "" { + t.Fatal("expected non empty trace header") + } + + spans := spanRecorder.Ended() + if got, expected := len(spans), 1; got != expected { + t.Fatalf("got %d spans, expected %d", got, expected) + } + if !spans[0].SpanContext().IsValid() { + t.Fatalf("invalid span created: %#v", spans[0].SpanContext()) + } + + d, err := ioutil.ReadAll(rr.Result().Body) + if err != nil { + t.Fatal(err) + } + if got, expected := string(d), "hello world"; got != expected { + t.Fatalf("got %q, expected %q", got, expected) + } +} diff --git a/instrumentation/net/http/otelhttp/test/transport_test.go b/instrumentation/net/http/otelhttp/test/transport_test.go new file mode 100644 index 000000000..000e6c763 --- /dev/null +++ b/instrumentation/net/http/otelhttp/test/transport_test.go @@ -0,0 +1,118 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" +) + +func TestTransportUsesFormatter(t *testing.T) { + prop := propagation.TraceContext{} + spanRecorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + content := []byte("Hello, world!") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + span := trace.SpanContextFromContext(ctx) + if !span.IsValid() { + t.Fatalf("invalid span wrapping handler: %#v", span) + } + if _, err := w.Write(content); err != nil { + t.Fatal(err) + } + })) + defer ts.Close() + + r, err := http.NewRequest(http.MethodGet, ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + tr := otelhttp.NewTransport( + http.DefaultTransport, + otelhttp.WithTracerProvider(provider), + otelhttp.WithPropagators(prop), + ) + + c := http.Client{Transport: tr} + res, err := c.Do(r) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + + spans := spanRecorder.Ended() + spanName := spans[0].Name() + expectedName := "HTTP GET" + if spanName != expectedName { + t.Fatalf("unexpected name: got %s, expected %s", spanName, expectedName) + } + +} + +func TestTransportErrorStatus(t *testing.T) { + // Prepare tracing stuff. + spanRecorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder)) + + // Run a server and stop to make sure nothing is listening and force the error. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + // Create our Transport and make request. + tr := otelhttp.NewTransport( + http.DefaultTransport, + otelhttp.WithTracerProvider(provider), + ) + c := http.Client{Transport: tr} + r, err := http.NewRequest(http.MethodGet, server.URL, nil) + if err != nil { + t.Fatal(err) + } + _, err = c.Do(r) + if err == nil { + t.Fatal("transport should have returned an error, it didn't") + } + + // Check span. + spans := spanRecorder.Ended() + if len(spans) != 1 { + t.Fatalf("expected 1 span; got: %d", len(spans)) + } + span := spans[0] + + if span.EndTime().IsZero() { + t.Errorf("span should be ended; it isn't") + } + + if got := span.Status().Code; got != codes.Error { + t.Errorf("expected error status code on span; got: %q", got) + } + + if got := span.Status().Description; !strings.Contains(got, "connect: connection refused") { + t.Errorf("expected error status message on span; got: %q", got) + } +} diff --git a/instrumentation/net/http/otelhttp/transport_test.go b/instrumentation/net/http/otelhttp/transport_test.go index 38d9456ec..eed7581c1 100644 --- a/instrumentation/net/http/otelhttp/transport_test.go +++ b/instrumentation/net/http/otelhttp/transport_test.go @@ -17,117 +17,21 @@ package otelhttp import ( "bytes" "context" - "fmt" + "errors" + "io" "io/ioutil" "net/http" "net/http/httptest" - "strings" "testing" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/oteltest" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) -func TestTransportBasics(t *testing.T) { - prop := propagation.TraceContext{} - provider := oteltest.NewTracerProvider() - content := []byte("Hello, world!") - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) - span := trace.SpanContextFromContext(ctx) - tgtID, err := trace.SpanIDFromHex(fmt.Sprintf("%016x", uint(2))) - if err != nil { - t.Fatalf("Error converting id to SpanID: %s", err.Error()) - } - if span.SpanID() != tgtID { - t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID(), tgtID) - } - if _, err := w.Write(content); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - r, err := http.NewRequest(http.MethodGet, ts.URL, nil) - if err != nil { - t.Fatal(err) - } - - tr := NewTransport( - http.DefaultTransport, - WithTracerProvider(provider), - WithPropagators(prop), - ) - - c := http.Client{Transport: tr} - res, err := c.Do(r) - if err != nil { - t.Fatal(err) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(body, content) { - t.Fatalf("unexpected content: got %s, expected %s", body, content) - } -} - -func TestNilTransport(t *testing.T) { - prop := propagation.TraceContext{} - provider := oteltest.NewTracerProvider() - content := []byte("Hello, world!") - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) - span := trace.SpanContextFromContext(ctx) - tgtID, err := trace.SpanIDFromHex(fmt.Sprintf("%016x", uint(2))) - if err != nil { - t.Fatalf("Error converting id to SpanID: %s", err.Error()) - } - if span.SpanID() != tgtID { - t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID(), tgtID) - } - if _, err := w.Write(content); err != nil { - t.Fatal(err) - } - })) - defer ts.Close() - - r, err := http.NewRequest(http.MethodGet, ts.URL, nil) - if err != nil { - t.Fatal(err) - } - - tr := NewTransport( - nil, - WithTracerProvider(provider), - WithPropagators(prop), - ) - - c := http.Client{Transport: tr} - res, err := c.Do(r) - if err != nil { - t.Fatal(err) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(body, content) { - t.Fatalf("unexpected content: got %s, expected %s", body, content) - } -} - func TestTransportFormatter(t *testing.T) { - var httpMethods = []struct { name string method string @@ -186,7 +90,7 @@ func TestTransportFormatter(t *testing.T) { if err != nil { t.Fatal(err) } - formattedName := defaultTransportFormatter("", r) + formattedName := "HTTP " + r.Method if formattedName != tc.expected { t.Fatalf("unexpected name: got %s, expected %s", formattedName, tc.expected) @@ -196,23 +100,22 @@ func TestTransportFormatter(t *testing.T) { } -func TestTransportUsesFormatter(t *testing.T) { +func TestTransportBasics(t *testing.T) { prop := propagation.TraceContext{} - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) content := []byte("Hello, world!") + ctx := context.Background() + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x01}, + SpanID: trace.SpanID{0x01}, + }) + ctx = trace.ContextWithRemoteSpanContext(ctx, sc) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) span := trace.SpanContextFromContext(ctx) - tgtID, err := trace.SpanIDFromHex(fmt.Sprintf("%016x", uint(2))) - if err != nil { - t.Fatalf("Error converting id to SpanID: %s", err.Error()) - } - if span.SpanID() != tgtID { - t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID(), tgtID) + if span.SpanID() != sc.SpanID() { + t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID(), sc.SpanID()) } if _, err := w.Write(content); err != nil { t.Fatal(err) @@ -220,125 +123,166 @@ func TestTransportUsesFormatter(t *testing.T) { })) defer ts.Close() - r, err := http.NewRequest(http.MethodGet, ts.URL, nil) + r, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil) if err != nil { t.Fatal(err) } - tr := NewTransport( - http.DefaultTransport, - WithTracerProvider(provider), - WithPropagators(prop), - ) + tr := NewTransport(http.DefaultTransport, WithPropagators(prop)) c := http.Client{Transport: tr} res, err := c.Do(r) if err != nil { t.Fatal(err) } - res.Body.Close() - spans := spanRecorder.Completed() - spanName := spans[0].Name() - expectedName := "HTTP GET" - if spanName != expectedName { - t.Fatalf("unexpected name: got %s, expected %s", spanName, expectedName) - } - -} - -func TestTransportErrorStatus(t *testing.T) { - // Prepare tracing stuff. - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) - - // Run a server and stop to make sure nothing is listening and force the error. - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - server.Close() - - // Create our Transport and make request. - tr := NewTransport( - http.DefaultTransport, - WithTracerProvider(provider), - ) - c := http.Client{Transport: tr} - r, err := http.NewRequest(http.MethodGet, server.URL, nil) + body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } - _, err = c.Do(r) - if err == nil { - t.Fatal("transport should have returned an error, it didn't") - } - // Check span. - gotSpans := spanRecorder.Completed() - if len(gotSpans) != 1 { - t.Fatalf("expected 1 span; got: %d", len(gotSpans)) - } - - spanEnded := gotSpans[0].Ended() - if !spanEnded { - t.Errorf("span should be ended; it isn't") - } - - spanStatusCode := gotSpans[0].StatusCode() - if spanStatusCode != codes.Error { - t.Errorf("expected error status code on span; got: %q", spanStatusCode) - } - - spanStatusMessage := gotSpans[0].StatusMessage() - if !strings.Contains(spanStatusMessage, "connect: connection refused") { - t.Errorf("expected error status message on span; got: %q", spanStatusMessage) + if !bytes.Equal(body, content) { + t.Fatalf("unexpected content: got %s, expected %s", body, content) } } -type testErrorReadCloser struct{} +func TestNilTransport(t *testing.T) { + prop := propagation.TraceContext{} + content := []byte("Hello, world!") -func (testErrorReadCloser) Read(p []byte) (n int, err error) { return 0, fmt.Errorf("something") } -func (testErrorReadCloser) Close() error { return nil } - -func TestWrappedBodyReadErrorStatus(t *testing.T) { - // Prepare tracing stuff. - spanRecorder := new(oteltest.SpanRecorder) - provider := oteltest.NewTracerProvider( - oteltest.WithSpanRecorder(spanRecorder), - ) - tracer := provider.Tracer("") ctx := context.Background() - _, span := tracer.Start(ctx, "test") + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x01}, + SpanID: trace.SpanID{0x01}, + }) + ctx = trace.ContextWithRemoteSpanContext(ctx, sc) - // Create our wrapper. - wb := wrappedBody{ - span: span, - body: testErrorReadCloser{}, - } - _, err := wb.Read([]byte{}) - if err == nil { - t.Fatalf("expected error while reading") - } - wb.Close() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + span := trace.SpanContextFromContext(ctx) + if span.SpanID() != sc.SpanID() { + t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID(), sc.SpanID()) + } + if _, err := w.Write(content); err != nil { + t.Fatal(err) + } + })) + defer ts.Close() - // Check span. - gotSpans := spanRecorder.Completed() - if len(gotSpans) != 1 { - t.Fatalf("expected 1 span; got: %d", len(gotSpans)) + r, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil) + if err != nil { + t.Fatal(err) } - spanEnded := gotSpans[0].Ended() - if !spanEnded { - t.Errorf("span should be ended; it isn't") + tr := NewTransport(nil, WithPropagators(prop)) + + c := http.Client{Transport: tr} + res, err := c.Do(r) + if err != nil { + t.Fatal(err) } - spanStatusCode := gotSpans[0].StatusCode() - if spanStatusCode != codes.Error { - t.Errorf("expected error status code on span; got: %q", spanStatusCode) + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) } - spanStatusMessage := gotSpans[0].StatusMessage() - if !strings.Contains(spanStatusMessage, "something") { - t.Errorf("expected error status message on span; got: %q", spanStatusMessage) + if !bytes.Equal(body, content) { + t.Fatalf("unexpected content: got %s, expected %s", body, content) } } + +const readSize = 42 + +type readCloser struct { + readErr, closeErr error +} + +func (rc readCloser) Read(p []byte) (n int, err error) { + return readSize, rc.readErr +} +func (rc readCloser) Close() error { + return rc.closeErr +} + +type span struct { + trace.Span + + ended bool + recordedErr error + + statusCode codes.Code + statusDesc string +} + +func (s *span) End(...trace.SpanEndOption) { + s.ended = true +} + +func (s *span) RecordError(err error, _ ...trace.EventOption) { + s.recordedErr = err +} + +func (s *span) SetStatus(c codes.Code, d string) { + s.statusCode, s.statusDesc = c, d +} + +func (s *span) assert(t *testing.T, ended bool, err error, c codes.Code, d string) { + if ended { + assert.True(t, s.ended, "not ended") + } else { + assert.False(t, s.ended, "ended") + } + + if err == nil { + assert.NoError(t, s.recordedErr, "recorded an error") + } else { + assert.Equal(t, err, s.recordedErr) + } + + assert.Equal(t, c, s.statusCode, "status codes not equal") + assert.Equal(t, d, s.statusDesc, "status description not equal") +} + +func TestWrappedBodyRead(t *testing.T) { + s := new(span) + wb := &wrappedBody{span: trace.Span(s), body: readCloser{}} + n, err := wb.Read([]byte{}) + assert.Equal(t, readSize, n, "wrappedBody returned wrong bytes") + assert.NoError(t, err) + s.assert(t, false, nil, codes.Unset, "") +} + +func TestWrappedBodyReadEOFError(t *testing.T) { + s := new(span) + wb := &wrappedBody{span: trace.Span(s), body: readCloser{readErr: io.EOF}} + n, err := wb.Read([]byte{}) + assert.Equal(t, readSize, n, "wrappedBody returned wrong bytes") + assert.Equal(t, io.EOF, err) + s.assert(t, true, nil, codes.Unset, "") +} + +func TestWrappedBodyReadError(t *testing.T) { + s := new(span) + expectedErr := errors.New("test") + wb := &wrappedBody{span: trace.Span(s), body: readCloser{readErr: expectedErr}} + n, err := wb.Read([]byte{}) + assert.Equal(t, readSize, n, "wrappedBody returned wrong bytes") + assert.Equal(t, expectedErr, err) + s.assert(t, false, expectedErr, codes.Error, expectedErr.Error()) +} + +func TestWrappedBodyClose(t *testing.T) { + s := new(span) + wb := &wrappedBody{span: trace.Span(s), body: readCloser{}} + assert.NoError(t, wb.Close()) + s.assert(t, true, nil, codes.Unset, "") +} + +func TestWrappedBodyCloseError(t *testing.T) { + s := new(span) + expectedErr := errors.New("test") + wb := &wrappedBody{span: trace.Span(s), body: readCloser{closeErr: expectedErr}} + assert.Equal(t, expectedErr, wb.Close()) + s.assert(t, true, nil, codes.Unset, "") +}