Skip to content

Commit

Permalink
otelhttp: add WithClientTrace to connect with otelhttptrace
Browse files Browse the repository at this point in the history
Transport can be created with `otelhttp.NewTransport(base, otelhttp.WithClientTrace(otelhttptrace.NewClientTrace))`
for additional httptrace tracing.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
tonistiigi committed Dec 1, 2021
1 parent 6c86403 commit aaa9769
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- Add `WithClientTrace` option to `otelhttp.Transport` (#875)

### Changed

- The `Transport`, `Handler`, and HTTP client convenience wrappers in the `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` package now use the `TracerProvider` from the parent context if one exists and none was explicitly set when configuring the instrumentation. (#873)
Expand Down
11 changes: 11 additions & 0 deletions instrumentation/net/http/otelhttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package otelhttp

import (
"context"
"net/http"
"net/http/httptrace"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
Expand All @@ -39,6 +41,7 @@ type config struct {
WriteEvent bool
Filters []Filter
SpanNameFormatter func(string, *http.Request) string
ClientTrace func(context.Context) *httptrace.ClientTrace

TracerProvider trace.TracerProvider
MeterProvider metric.MeterProvider
Expand Down Expand Up @@ -174,3 +177,11 @@ func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Opt
c.SpanNameFormatter = f
})
}

// WithClientTrace takes a function that returns client trace instance that will be
// applied to the requests sent through the otelhttp Transport.
func WithClientTrace(f func(context.Context) *httptrace.ClientTrace) Option {
return optionFunc(func(c *config) {
c.ClientTrace = f
})
}
63 changes: 63 additions & 0 deletions instrumentation/net/http/otelhttp/test/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"strings"
"testing"

Expand Down Expand Up @@ -166,3 +167,65 @@ func TestTransportRequestWithTraceContext(t *testing.T) {
assert.NotEmpty(t, spans[1].Parent().SpanID())
assert.Equal(t, spans[0].SpanContext().SpanID(), spans[1].Parent().SpanID())
}

func TestWithHTTPTrace(t *testing.T) {
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) {
_, err := w.Write(content)
require.NoError(t, err)
}))
defer ts.Close()

tracer := provider.Tracer("")
ctx, span := tracer.Start(context.Background(), "test_span")

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
require.NoError(t, err)

r = r.WithContext(ctx)

clientTracer := func(ctx context.Context) *httptrace.ClientTrace {
var span trace.Span
return &httptrace.ClientTrace{
GetConn: func(_ string) {
_, span = trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "httptrace.GetConn")
},
GotConn: func(_ httptrace.GotConnInfo) {
if span != nil {
span.End()
}
},
}
}

tr := otelhttp.NewTransport(
http.DefaultTransport,
otelhttp.WithClientTrace(clientTracer),
)

c := http.Client{Transport: tr}
res, err := c.Do(r)
require.NoError(t, err)

span.End()

body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)

require.Equal(t, content, body)

spans := spanRecorder.Ended()
require.Len(t, spans, 3)

assert.Equal(t, "httptrace.GetConn", spans[0].Name())
assert.Equal(t, "test_span", spans[1].Name())
assert.Equal(t, "HTTP GET", spans[2].Name())
assert.NotEmpty(t, spans[1].Parent().SpanID())
assert.Equal(t, spans[2].SpanContext().SpanID(), spans[0].Parent().SpanID())
assert.Equal(t, spans[1].SpanContext().SpanID(), spans[2].Parent().SpanID())
}
7 changes: 7 additions & 0 deletions instrumentation/net/http/otelhttp/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"io"
"net/http"
"net/http/httptrace"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
Expand All @@ -36,6 +37,7 @@ type Transport struct {
spanStartOptions []trace.SpanStartOption
filters []Filter
spanNameFormatter func(string, *http.Request) string
clientTrace func(context.Context) *httptrace.ClientTrace
}

var _ http.RoundTripper = &Transport{}
Expand Down Expand Up @@ -71,6 +73,7 @@ func (t *Transport) applyConfig(c *config) {
t.spanStartOptions = c.SpanStartOptions
t.filters = c.Filters
t.spanNameFormatter = c.SpanNameFormatter
t.clientTrace = c.ClientTrace
}

func defaultTransportFormatter(_ string, r *http.Request) string {
Expand Down Expand Up @@ -102,6 +105,10 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {

ctx, span := tracer.Start(r.Context(), t.spanNameFormatter("", r), opts...)

if t.clientTrace != nil {
ctx = httptrace.WithClientTrace(ctx, t.clientTrace(ctx))
}

r = r.WithContext(ctx)
span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...)
t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header))
Expand Down

0 comments on commit aaa9769

Please sign in to comment.