Skip to content
This repository has been archived by the owner on Jul 31, 2023. It is now read-only.

adds ClientTracer option to allow usage of httptrace package with ochttp #848

Merged
merged 3 commits into from
Aug 3, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugin/ochttp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package ochttp

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

"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
Expand Down Expand Up @@ -51,6 +52,11 @@ type Transport struct {
// name equals the URL Path.
FormatSpanName func(*http.Request) string

// ClientTracer may be set to a function allowing OpenCensus to annotate the
// current span with HTTP request event information emitted by the httptrace
// package.
ClientTracer func(*http.Request, *trace.Span) *httptrace.ClientTrace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point with the name is that this is not the ClientTrace itself - it is a constructor function for the ClientTrace - so I think the name "NewClientTrace" is more appropriate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewClientTrace doesn't sit well with me as a property of the Transport struct but I'll admit I see your point on ClientTracer being not great either. I'll use NewClientTrace.


// TODO: Implement tag propagation for HTTP.
}

Expand All @@ -77,6 +83,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
SpanKind: trace.SpanKindClient,
},
formatSpanName: spanNameFormatter,
clientTracer: t.ClientTracer,
}
rt = statsTransport{base: rt}
return rt.RoundTrip(req)
Expand Down
175 changes: 175 additions & 0 deletions plugin/ochttp/span_annotator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2018, OpenCensus 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 ochttp

import (
"crypto/tls"
"net/http"
"net/http/httptrace"
"strings"

"go.opencensus.io/trace"
)

// SpanAnnotator implements the annotation of all available hooks for
// httptrace.ClientTrace.
type SpanAnnotator struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this needs to be public. If they want to use only a subset of the hooks, they can simply set the corresponding fields to nil in the returned ClientTrace. Likewise, if they want to use their own ClientTrace and copy across some of the fields, they can do this too by calling methods on ClientTrace directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it is a matter of opinion on what is the friendlier method for consumers.

Your preference is eliminating the hooks not desired:

func NewSpanAnnotatorWrapper(req *http.Request, s *trace.Span) *httptrace.ClientTrace {
    clientTrace := ochttp.NewSpanAnnotator(req, s)

    // deregister unwanted hooks
    clientTrace.GetConn = nil 
    clientTrace.Got100Continue = nil
    ...
    return clientTrace
}

...

transport := ochttp.Transport{
    NewClientTrace: NewSpanAnnotatorWrapper,
}

Mine is choosing the hooks that are:

func NewSpanAnnotator(req *http.Request, s *trace.Span) *httptrace.ClientTrace {
    spanAnnotator := ochttp.SpanAnnotator{s}

    // register wanted hooks
    return &httptrace.ClientTrace{
        GetConn:              spanAnnotator.PutIdleConn,
        GotFirstResponseByte: spanAnnotator.GotFirstResponseByte,
        DNSStart:             spanAnnotator.DNSStart,
        ...
    }
}

...

transport := ochttp.Transport{
    ClientTracer: NewSpanAnnotator,
}

Which reminds me why I wanted SpanAnnotator to embed *trace.Span or otherwise have it be an exported property. Bear in mind that the actual resulting *httptrace.ClientTrace or SpanAnnotator is created and contained within ochttp's traceTransport.RoundTrip().

Copy link
Contributor

@semistrict semistrict Aug 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't see why the type needs to be public. You could do this :

t := &ochttp.Transport {
    ClientTracer: func(r *http.Request, s *trace.Span) *httptrace.ClientTrace {
 ann := ochttp.NewSpanAnnotator(r,s)          
 return &httptrace.ClientTrace {
               GetConn: ann.GetConn
          }
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a type adds complexity and distraction to the godoc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I seemed to have forgotten because the function outputs *httptrace.ClientTrace I can always wrap it with a new one. Good thing vacation is coming up for some fresh eyes.

Will not only unexport SpanAnnotator but all its methods can be unexported too.

sp *trace.Span
}

// NewSpanAnnotator returns a httptrace.ClientTrace which annotates all emitted
// httptrace events on the provided Span.
func NewSpanAnnotator(req *http.Request, s *trace.Span) *httptrace.ClientTrace {
spanAnnotator := SpanAnnotator{s}

return &httptrace.ClientTrace{
GetConn: spanAnnotator.GetConn,
GotConn: spanAnnotator.GotConn,
PutIdleConn: spanAnnotator.PutIdleConn,
GotFirstResponseByte: spanAnnotator.GotFirstResponseByte,
Got100Continue: spanAnnotator.Got100Continue,
DNSStart: spanAnnotator.DNSStart,
DNSDone: spanAnnotator.DNSDone,
ConnectStart: spanAnnotator.ConnectStart,
ConnectDone: spanAnnotator.ConnectDone,
TLSHandshakeStart: spanAnnotator.TLSHandshakeStart,
TLSHandshakeDone: spanAnnotator.TLSHandshakeDone,
WroteHeaders: spanAnnotator.WroteHeaders,
Wait100Continue: spanAnnotator.Wait100Continue,
WroteRequest: spanAnnotator.WroteRequest,
}
}

// GetConn implements a httptrace.ClientTrace hook
func (s SpanAnnotator) GetConn(hostPort string) {
attrs := []trace.Attribute{
trace.StringAttribute("httptrace.get_connection.host_port", hostPort),
}
s.sp.Annotate(attrs, "GetConn")
}

// GotConn implements a httptrace.ClientTrace hook
func (s SpanAnnotator) GotConn(info httptrace.GotConnInfo) {
attrs := []trace.Attribute{
trace.BoolAttribute("httptrace.got_connection.reused", info.Reused),
trace.BoolAttribute("httptrace.got_connection.was_idle", info.WasIdle),
}
if info.WasIdle {
attrs = append(attrs,
trace.StringAttribute("httptrace.got_connection.idle_time", info.IdleTime.String()))
}
s.sp.Annotate(attrs, "GotConn")
}

// PutIdleConn implements a httptrace.ClientTrace hook
func (s SpanAnnotator) PutIdleConn(err error) {
var attrs []trace.Attribute
if err != nil {
attrs = append(attrs,
trace.StringAttribute("httptrace.put_idle_connection.error", err.Error()))
}
s.sp.Annotate(attrs, "PutIdleConn")
}

// GotFirstResponseByte implements a httptrace.ClientTrace hook
func (s SpanAnnotator) GotFirstResponseByte() {
s.sp.Annotate(nil, "GotFirstResponseByte")
}

// Got100Continue implements a httptrace.ClientTrace hook
func (s SpanAnnotator) Got100Continue() {
s.sp.Annotate(nil, "Got100Continue")
}

// DNSStart implements a httptrace.ClientTrace hook
func (s SpanAnnotator) DNSStart(info httptrace.DNSStartInfo) {
attrs := []trace.Attribute{
trace.StringAttribute("httptrace.dns_start.host", info.Host),
}
s.sp.Annotate(attrs, "DNSStart")
}

// DNSDone implements a httptrace.ClientTrace hook
func (s SpanAnnotator) DNSDone(info httptrace.DNSDoneInfo) {
var addrs []string
for _, addr := range info.Addrs {
addrs = append(addrs, addr.String())
}
attrs := []trace.Attribute{
trace.StringAttribute("httptrace.dns_done.addrs", strings.Join(addrs, " , ")),
}
if info.Err != nil {
attrs = append(attrs,
trace.StringAttribute("httptrace.dns_done.error", info.Err.Error()))
}
s.sp.Annotate(attrs, "DNSDone")
}

// ConnectStart implements a httptrace.ClientTrace hook
func (s SpanAnnotator) ConnectStart(network, addr string) {
attrs := []trace.Attribute{
trace.StringAttribute("httptrace.connect_start.network", network),
trace.StringAttribute("httptrace.connect_start.addr", addr),
}
s.sp.Annotate(attrs, "ConnectStart")
}

// ConnectDone implements a httptrace.ClientTrace hook
func (s SpanAnnotator) ConnectDone(network, addr string, err error) {
attrs := []trace.Attribute{
trace.StringAttribute("httptrace.connect_done.network", network),
trace.StringAttribute("httptrace.connect_done.addr", addr),
}
if err != nil {
attrs = append(attrs,
trace.StringAttribute("httptrace.connect_done.error", err.Error()))
}
s.sp.Annotate(attrs, "ConnectDone")
}

// TLSHandshakeStart implements a httptrace.ClientTrace hook
func (s SpanAnnotator) TLSHandshakeStart() {
s.sp.Annotate(nil, "TLSHandshakeStart")
}

// TLSHandshakeDone implements a httptrace.ClientTrace hook
func (s SpanAnnotator) TLSHandshakeDone(_ tls.ConnectionState, err error) {
var attrs []trace.Attribute
if err != nil {
attrs = append(attrs,
trace.StringAttribute("httptrace.tls_handshake_done.error", err.Error()))
}
s.sp.Annotate(attrs, "TLSHandshakeDone")
}

// WroteHeaders implements a httptrace.ClientTrace hook
func (s SpanAnnotator) WroteHeaders() {
s.sp.Annotate(nil, "WroteHeaders")
}

// Wait100Continue implements a httptrace.ClientTrace hook
func (s SpanAnnotator) Wait100Continue() {
s.sp.Annotate(nil, "Wait100Continue")
}

// WroteRequest implements a httptrace.ClientTrace hook
func (s SpanAnnotator) WroteRequest(info httptrace.WroteRequestInfo) {
var attrs []trace.Attribute
if info.Err != nil {
attrs = append(attrs,
trace.StringAttribute("httptrace.wrote_request.error", info.Err.Error()))
}
s.sp.Annotate(attrs, "WroteRequest")
}
104 changes: 104 additions & 0 deletions plugin/ochttp/span_annotator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2018, OpenCensus 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 ochttp_test

import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"

"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/trace"
)

func TestSpanAnnotator(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Write([]byte("Hello, world!"))
}))
defer server.Close()

recorder := &testExporter{}

trace.RegisterExporter(recorder)

tr := ochttp.Transport{ClientTracer: ochttp.NewSpanAnnotator}

req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body"))
if err != nil {
t.Errorf("error creating request: %v", err)
}

resp, err := tr.RoundTrip(req)
if err != nil {
t.Errorf("response error: %v", err)
}
if err := resp.Body.Close(); err != nil {
t.Errorf("error closing response body: %v", err)
}
if got, want := resp.StatusCode, 200; got != want {
t.Errorf("resp.StatusCode=%d; want=%d", got, want)
}

if got, want := len(recorder.spans), 1; got != want {
t.Errorf("span count=%d; want=%d", got, want)
}

var annotations []string
for _, annotation := range recorder.spans[0].Annotations {
annotations = append(annotations, annotation.Message)
}

required := []string{
"GetConn", "GotConn", "GotFirstResponseByte", "ConnectStart",
"ConnectDone", "WroteHeaders", "WroteRequest",
}

if errs := requiredAnnotations(required, annotations); len(errs) > 0 {
for _, err := range errs {
t.Error(err)
}
}

}

type testExporter struct {
mu sync.Mutex
spans []*trace.SpanData
}

func (t *testExporter) ExportSpan(s *trace.SpanData) {
t.mu.Lock()
t.spans = append(t.spans, s)
t.mu.Unlock()
}

func requiredAnnotations(required []string, list []string) []error {
var errs []error
for _, item := range required {
var found bool
for _, v := range list {
if v == item {
found = true
}
}
if !found {
errs = append(errs, errors.New("missing expected annotation: "+item))
}
}
return errs
}
9 changes: 8 additions & 1 deletion plugin/ochttp/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package ochttp
import (
"io"
"net/http"
"net/http/httptrace"

"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/trace"
Expand All @@ -42,6 +43,7 @@ type traceTransport struct {
startOptions trace.StartOptions
format propagation.HTTPFormat
formatSpanName func(*http.Request) string
clientTracer func(*http.Request, *trace.Span) *httptrace.ClientTrace
}

// TODO(jbd): Add message events for request and response size.
Expand All @@ -57,7 +59,12 @@ func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
trace.WithSampler(t.startOptions.Sampler),
trace.WithSpanKind(trace.SpanKindClient))

req = req.WithContext(ctx)
if t.clientTracer != nil {
req = req.WithContext(httptrace.WithClientTrace(ctx, t.clientTracer(req, span)))
} else {
req = req.WithContext(ctx)
}

if t.format != nil {
t.format.SpanContextToRequest(span.SpanContext(), req)
}
Expand Down