Skip to content
Permalink
Browse files

net/http, net/http/httptrace: new package for tracing HTTP client req…

…uests

Updates #12580

Change-Id: I9f9578148ef2b48dffede1007317032d39f6af55
Reviewed-on: https://go-review.googlesource.com/22191
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Tom Bergan <tombergan@google.com>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
  • Loading branch information...
bradfitz committed Apr 16, 2016
1 parent 1b591df commit 1518d431321100cd9f0e18d740da7c835ba438dd
@@ -282,6 +282,7 @@ var pkgDeps = map[string][]string{
// do networking portably, it must have a small dependency set: just L0+basic os.
"net": {"L0", "CGO",
"context", "math/rand", "os", "sort", "syscall", "time",
"internal/nettrace",
"internal/syscall/windows", "internal/singleflight", "internal/race"},

// NET enables use of basic network-related packages.
@@ -363,8 +364,11 @@ var pkgDeps = map[string][]string{
"mime/multipart", "runtime/debug",
"net/http/internal",
"golang.org/x/net/http2/hpack",
"internal/nettrace",
"net/http/httptrace",
},
"net/http/internal": {"L4"},
"net/http/internal": {"L4"},
"net/http/httptrace": {"context", "internal/nettrace", "net", "reflect", "time"},

// HTTP-using packages.
"expvar": {"L4", "OS", "encoding/json", "net/http"},
@@ -0,0 +1,43 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package nettrace contains internal hooks for tracing activity in
// the net package. This package is purely internal for use by the
// net/http/httptrace package and has no stable API exposed to end
// users.
package nettrace

// TraceKey is a context.Context Value key. Its associated value should
// be a *Trace struct.
type TraceKey struct{}

// LookupIPAltResolverKey is a context.Context Value key used by tests to
// specify an alternate resolver func.
// It is not exposed to outsider users. (But see issue 12503)
// The value should be the same type as lookupIP:
// func lookupIP(ctx context.Context, host string) ([]IPAddr, error)
type LookupIPAltResolverKey struct{}

// Trace contains a set of hooks for tracing events within
// the net package. Any specific hook may be nil.
type Trace struct {
// DNSStart is called with the hostname of a DNS lookup
// before it begins.
DNSStart func(name string)

// DNSDone is called after a DNS lookup completes (or fails).
// The coalesced parameter is whether singleflight de-dupped
// the call. The addrs are of type net.IPAddr but can't
// actually be for circular dependency reasons.
DNSDone func(netIPs []interface{}, coalesced bool, err error)

// ConnectStart is called before a Dial. In the case of
// DualStack (Happy Eyeballs) dialing, this may be called
// multiple times, from multiple goroutines.
ConnectStart func(network, addr string)

// ConnectStart is called after a Dial with the results. It
// may also be called multiple times, like ConnectStart.
ConnectDone func(network, addr string, err error)
}
@@ -6,6 +6,7 @@ package net

import (
"context"
"internal/nettrace"
"time"
)

@@ -474,6 +475,16 @@ func dialSerial(ctx context.Context, dp *dialParam, ras addrList) (Conn, error)
// dialSingle attempts to establish and returns a single connection to
// the destination address.
func dialSingle(ctx context.Context, dp *dialParam, ra Addr) (c Conn, err error) {
trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace)
if trace != nil {
raStr := ra.String()
if trace.ConnectStart != nil {
trace.ConnectStart(dp.network, raStr)
}
if trace.ConnectDone != nil {
defer func() { trace.ConnectDone(dp.network, raStr, err) }()
}
}
la := dp.LocalAddr
switch ra := ra.(type) {
case *TCPAddr:
@@ -0,0 +1,225 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.h

// Package httptrace provides mechanisms to trace the events within
// HTTP client requests.
package httptrace

import (
"context"
"internal/nettrace"
"net"
"reflect"
"time"
)

// unique type to prevent assignment.
type clientEventContextKey struct{}

// ContextClientTrace returns the ClientTrace associated with the
// provided context. If none, it returns nil.
func ContextClientTrace(ctx context.Context) *ClientTrace {
trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
return trace
}

// WithClientTrace returns a new context based on the provided parent
// ctx. HTTP client requests made with the returned context will use
// the provided trace hooks, in addition to any previous hooks
// registered with ctx. Any hooks defined in the provided trace will
// be called first.
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
if trace == nil {
panic("nil trace")
}
old := ContextClientTrace(ctx)
trace.compose(old)

ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
if trace.hasNetHooks() {
nt := &nettrace.Trace{
ConnectStart: trace.ConnectStart,
ConnectDone: trace.ConnectDone,
}
if trace.DNSStart != nil {
nt.DNSStart = func(name string) {
trace.DNSStart(DNSStartInfo{Host: name})
}
}
if trace.DNSDone != nil {
nt.DNSDone = func(netIPs []interface{}, coalesced bool, err error) {
addrs := make([]net.IPAddr, len(netIPs))
for i, ip := range netIPs {
addrs[i] = ip.(net.IPAddr)
}
trace.DNSDone(DNSDoneInfo{
Addrs: addrs,
Coalesced: coalesced,
Err: err,
})
}
}
ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
}
return ctx
}

// ClientTrace is a set of hooks to run at various stages of an HTTP
// client request. Any particular hook may be nil. Functions may be
// called concurrently from different goroutines, starting after the
// call to Transport.RoundTrip and ending either when RoundTrip
// returns an error, or when the Response.Body is closed.
type ClientTrace struct {
// GetConn is called before a connection is created or
// retrieved from an idle pool. The hostPort is the
// "host:port" of the target or proxy. GetConn is called even
// if there's already an idle cached connection available.
GetConn func(hostPort string)

// GotConn is called after a successful connection is
// obtained. There is no hook for failure to obtain a
// connection; instead, use the error from
// Transport.RoundTrip.
GotConn func(GotConnInfo)

// PutIdleConn is called when the connection is returned to
// the idle pool. If err is nil, the connection was
// successfully returned to the idle pool. If err is non-nil,
// it describes why not. PutIdleConn is not called if
// connection reuse is disabled via Transport.DisableKeepAlives.
// PutIdleConn is called before the caller's Response.Body.Close
// call returns.
PutIdleConn func(err error)

// GotFirstResponseByte is called when the first byte of the response
// headers is available.
GotFirstResponseByte func()

// Got100Continue is called if the server replies with a "100
// Continue" response.
Got100Continue func()

// DNSStart is called when a DNS lookup begins.
DNSStart func(DNSStartInfo)

// DNSDone is called when a DNS lookup ends.
DNSDone func(DNSDoneInfo)

// ConnectStart is called when a new connection's Dial begins.
// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
// enabled, this may be called multiple times.
ConnectStart func(network, addr string)

// ConnectDone is called when a new connection's Dial
// completes. The provided err indicates whether the
// connection completedly successfully.
// If net.Dialer.DualStack ("Happy Eyeballs") support is
// enabled, this may be called multiple times.
ConnectDone func(network, addr string, err error)

// WroteHeaders is called after the Transport has written
// the request headers.
WroteHeaders func()

// Wait100Continue is called if the Request specified
// "Expected: 100-continue" and the Transport has written the
// request headers but is waiting for "100 Continue" from the
// server before writing the request body.
Wait100Continue func()

// WroteRequest is called with the result of writing the
// request and any body.
WroteRequest func(WroteRequestInfo)
}

// WroteRequestInfo contains information provided to the WroteRequest
// hook.
type WroteRequestInfo struct {
// Err is any error encountered while writing the Request.
Err error
}

// compose modifies t such that it respects the previously-registered hooks in old,
// subject to the composition policy requested in t.Compose.
func (t *ClientTrace) compose(old *ClientTrace) {
if old == nil {
return
}
tv := reflect.ValueOf(t).Elem()
ov := reflect.ValueOf(old).Elem()
structType := tv.Type()
for i := 0; i < structType.NumField(); i++ {
tf := tv.Field(i)
hookType := tf.Type()
if hookType.Kind() != reflect.Func {
continue
}
of := ov.Field(i)
if of.IsNil() {
continue
}
if tf.IsNil() {
tf.Set(of)
continue
}

// Make a copy of tf for tf to call. (Otherwise it
// creates a recursive call cycle and stack overflows)
tfCopy := reflect.ValueOf(tf.Interface())

// We need to call both tf and of in some order.
newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value {
tfCopy.Call(args)
return of.Call(args)
})
tv.Field(i).Set(newFunc)
}
}

// DNSStartInfo contains information about a DNS request.
type DNSStartInfo struct {
Host string
}

// DNSDoneInfo contains information about the results of a DNS lookup.
type DNSDoneInfo struct {
// Addrs are the IPv4 and/or IPv6 addresses found in the DNS
// lookup. The contents of the slice should not be mutated.
Addrs []net.IPAddr

// Err is any error that occurred during the DNS lookup.
Err error

// Coalesced is whether the Addrs were shared with another
// caller who was doing the same DNS lookup concurrently.
Coalesced bool
}

func (t *ClientTrace) hasNetHooks() bool {
if t == nil {
return false
}
return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
}

// GotConnInfo is the argument to the ClientTrace.GotConn function and
// contains information about the obtained connection.
type GotConnInfo struct {
// Conn is the connection that was obtained. It is owned by
// the http.Transport and should not be read, written or
// closed by users of ClientTrace.
Conn net.Conn

// Reused is whether this connection has been previously
// used for another HTTP request.
Reused bool

// WasIdle is whether this connection was obtained from an
// idle pool.
WasIdle bool

// IdleTime reports how long the connection was previously
// idle, if WasIdle is true.
IdleTime time.Duration
}
@@ -0,0 +1,62 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.h

package httptrace

import (
"bytes"
"testing"
)

func TestCompose(t *testing.T) {
var buf bytes.Buffer
var testNum int

connectStart := func(b byte) func(network, addr string) {
return func(network, addr string) {
if addr != "addr" {
t.Errorf(`%d. args for %Q case = %q, %q; want addr of "addr"`, testNum, b, network, addr)
}
buf.WriteByte(b)
}
}

tests := [...]struct {
trace, old *ClientTrace
want string
}{
0: {
want: "T",
trace: &ClientTrace{
ConnectStart: connectStart('T'),
},
},
1: {
want: "TO",
trace: &ClientTrace{
ConnectStart: connectStart('T'),
},
old: &ClientTrace{ConnectStart: connectStart('O')},
},
2: {
want: "O",
trace: &ClientTrace{},
old: &ClientTrace{ConnectStart: connectStart('O')},
},
}
for i, tt := range tests {
testNum = i
buf.Reset()

tr := *tt.trace
tr.compose(tt.old)
if tr.ConnectStart != nil {
tr.ConnectStart("net", "addr")
}
if got := buf.String(); got != tt.want {
t.Errorf("%d. got = %q; want %q", i, got, tt.want)
}
}

}
Oops, something went wrong.

0 comments on commit 1518d43

Please sign in to comment.
You can’t perform that action at this time.