Skip to content

Commit 1518d43

Browse files
committed
net/http, net/http/httptrace: new package for tracing HTTP client requests
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>
1 parent 1b591df commit 1518d43

File tree

9 files changed

+573
-34
lines changed

9 files changed

+573
-34
lines changed

src/go/build/deps_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ var pkgDeps = map[string][]string{
282282
// do networking portably, it must have a small dependency set: just L0+basic os.
283283
"net": {"L0", "CGO",
284284
"context", "math/rand", "os", "sort", "syscall", "time",
285+
"internal/nettrace",
285286
"internal/syscall/windows", "internal/singleflight", "internal/race"},
286287

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

369373
// HTTP-using packages.
370374
"expvar": {"L4", "OS", "encoding/json", "net/http"},

src/internal/nettrace/nettrace.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package nettrace contains internal hooks for tracing activity in
6+
// the net package. This package is purely internal for use by the
7+
// net/http/httptrace package and has no stable API exposed to end
8+
// users.
9+
package nettrace
10+
11+
// TraceKey is a context.Context Value key. Its associated value should
12+
// be a *Trace struct.
13+
type TraceKey struct{}
14+
15+
// LookupIPAltResolverKey is a context.Context Value key used by tests to
16+
// specify an alternate resolver func.
17+
// It is not exposed to outsider users. (But see issue 12503)
18+
// The value should be the same type as lookupIP:
19+
// func lookupIP(ctx context.Context, host string) ([]IPAddr, error)
20+
type LookupIPAltResolverKey struct{}
21+
22+
// Trace contains a set of hooks for tracing events within
23+
// the net package. Any specific hook may be nil.
24+
type Trace struct {
25+
// DNSStart is called with the hostname of a DNS lookup
26+
// before it begins.
27+
DNSStart func(name string)
28+
29+
// DNSDone is called after a DNS lookup completes (or fails).
30+
// The coalesced parameter is whether singleflight de-dupped
31+
// the call. The addrs are of type net.IPAddr but can't
32+
// actually be for circular dependency reasons.
33+
DNSDone func(netIPs []interface{}, coalesced bool, err error)
34+
35+
// ConnectStart is called before a Dial. In the case of
36+
// DualStack (Happy Eyeballs) dialing, this may be called
37+
// multiple times, from multiple goroutines.
38+
ConnectStart func(network, addr string)
39+
40+
// ConnectStart is called after a Dial with the results. It
41+
// may also be called multiple times, like ConnectStart.
42+
ConnectDone func(network, addr string, err error)
43+
}

src/net/dial.go

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package net
66

77
import (
88
"context"
9+
"internal/nettrace"
910
"time"
1011
)
1112

@@ -474,6 +475,16 @@ func dialSerial(ctx context.Context, dp *dialParam, ras addrList) (Conn, error)
474475
// dialSingle attempts to establish and returns a single connection to
475476
// the destination address.
476477
func dialSingle(ctx context.Context, dp *dialParam, ra Addr) (c Conn, err error) {
478+
trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace)
479+
if trace != nil {
480+
raStr := ra.String()
481+
if trace.ConnectStart != nil {
482+
trace.ConnectStart(dp.network, raStr)
483+
}
484+
if trace.ConnectDone != nil {
485+
defer func() { trace.ConnectDone(dp.network, raStr, err) }()
486+
}
487+
}
477488
la := dp.LocalAddr
478489
switch ra := ra.(type) {
479490
case *TCPAddr:

src/net/http/httptrace/trace.go

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.h
4+
5+
// Package httptrace provides mechanisms to trace the events within
6+
// HTTP client requests.
7+
package httptrace
8+
9+
import (
10+
"context"
11+
"internal/nettrace"
12+
"net"
13+
"reflect"
14+
"time"
15+
)
16+
17+
// unique type to prevent assignment.
18+
type clientEventContextKey struct{}
19+
20+
// ContextClientTrace returns the ClientTrace associated with the
21+
// provided context. If none, it returns nil.
22+
func ContextClientTrace(ctx context.Context) *ClientTrace {
23+
trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
24+
return trace
25+
}
26+
27+
// WithClientTrace returns a new context based on the provided parent
28+
// ctx. HTTP client requests made with the returned context will use
29+
// the provided trace hooks, in addition to any previous hooks
30+
// registered with ctx. Any hooks defined in the provided trace will
31+
// be called first.
32+
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
33+
if trace == nil {
34+
panic("nil trace")
35+
}
36+
old := ContextClientTrace(ctx)
37+
trace.compose(old)
38+
39+
ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
40+
if trace.hasNetHooks() {
41+
nt := &nettrace.Trace{
42+
ConnectStart: trace.ConnectStart,
43+
ConnectDone: trace.ConnectDone,
44+
}
45+
if trace.DNSStart != nil {
46+
nt.DNSStart = func(name string) {
47+
trace.DNSStart(DNSStartInfo{Host: name})
48+
}
49+
}
50+
if trace.DNSDone != nil {
51+
nt.DNSDone = func(netIPs []interface{}, coalesced bool, err error) {
52+
addrs := make([]net.IPAddr, len(netIPs))
53+
for i, ip := range netIPs {
54+
addrs[i] = ip.(net.IPAddr)
55+
}
56+
trace.DNSDone(DNSDoneInfo{
57+
Addrs: addrs,
58+
Coalesced: coalesced,
59+
Err: err,
60+
})
61+
}
62+
}
63+
ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
64+
}
65+
return ctx
66+
}
67+
68+
// ClientTrace is a set of hooks to run at various stages of an HTTP
69+
// client request. Any particular hook may be nil. Functions may be
70+
// called concurrently from different goroutines, starting after the
71+
// call to Transport.RoundTrip and ending either when RoundTrip
72+
// returns an error, or when the Response.Body is closed.
73+
type ClientTrace struct {
74+
// GetConn is called before a connection is created or
75+
// retrieved from an idle pool. The hostPort is the
76+
// "host:port" of the target or proxy. GetConn is called even
77+
// if there's already an idle cached connection available.
78+
GetConn func(hostPort string)
79+
80+
// GotConn is called after a successful connection is
81+
// obtained. There is no hook for failure to obtain a
82+
// connection; instead, use the error from
83+
// Transport.RoundTrip.
84+
GotConn func(GotConnInfo)
85+
86+
// PutIdleConn is called when the connection is returned to
87+
// the idle pool. If err is nil, the connection was
88+
// successfully returned to the idle pool. If err is non-nil,
89+
// it describes why not. PutIdleConn is not called if
90+
// connection reuse is disabled via Transport.DisableKeepAlives.
91+
// PutIdleConn is called before the caller's Response.Body.Close
92+
// call returns.
93+
PutIdleConn func(err error)
94+
95+
// GotFirstResponseByte is called when the first byte of the response
96+
// headers is available.
97+
GotFirstResponseByte func()
98+
99+
// Got100Continue is called if the server replies with a "100
100+
// Continue" response.
101+
Got100Continue func()
102+
103+
// DNSStart is called when a DNS lookup begins.
104+
DNSStart func(DNSStartInfo)
105+
106+
// DNSDone is called when a DNS lookup ends.
107+
DNSDone func(DNSDoneInfo)
108+
109+
// ConnectStart is called when a new connection's Dial begins.
110+
// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
111+
// enabled, this may be called multiple times.
112+
ConnectStart func(network, addr string)
113+
114+
// ConnectDone is called when a new connection's Dial
115+
// completes. The provided err indicates whether the
116+
// connection completedly successfully.
117+
// If net.Dialer.DualStack ("Happy Eyeballs") support is
118+
// enabled, this may be called multiple times.
119+
ConnectDone func(network, addr string, err error)
120+
121+
// WroteHeaders is called after the Transport has written
122+
// the request headers.
123+
WroteHeaders func()
124+
125+
// Wait100Continue is called if the Request specified
126+
// "Expected: 100-continue" and the Transport has written the
127+
// request headers but is waiting for "100 Continue" from the
128+
// server before writing the request body.
129+
Wait100Continue func()
130+
131+
// WroteRequest is called with the result of writing the
132+
// request and any body.
133+
WroteRequest func(WroteRequestInfo)
134+
}
135+
136+
// WroteRequestInfo contains information provided to the WroteRequest
137+
// hook.
138+
type WroteRequestInfo struct {
139+
// Err is any error encountered while writing the Request.
140+
Err error
141+
}
142+
143+
// compose modifies t such that it respects the previously-registered hooks in old,
144+
// subject to the composition policy requested in t.Compose.
145+
func (t *ClientTrace) compose(old *ClientTrace) {
146+
if old == nil {
147+
return
148+
}
149+
tv := reflect.ValueOf(t).Elem()
150+
ov := reflect.ValueOf(old).Elem()
151+
structType := tv.Type()
152+
for i := 0; i < structType.NumField(); i++ {
153+
tf := tv.Field(i)
154+
hookType := tf.Type()
155+
if hookType.Kind() != reflect.Func {
156+
continue
157+
}
158+
of := ov.Field(i)
159+
if of.IsNil() {
160+
continue
161+
}
162+
if tf.IsNil() {
163+
tf.Set(of)
164+
continue
165+
}
166+
167+
// Make a copy of tf for tf to call. (Otherwise it
168+
// creates a recursive call cycle and stack overflows)
169+
tfCopy := reflect.ValueOf(tf.Interface())
170+
171+
// We need to call both tf and of in some order.
172+
newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value {
173+
tfCopy.Call(args)
174+
return of.Call(args)
175+
})
176+
tv.Field(i).Set(newFunc)
177+
}
178+
}
179+
180+
// DNSStartInfo contains information about a DNS request.
181+
type DNSStartInfo struct {
182+
Host string
183+
}
184+
185+
// DNSDoneInfo contains information about the results of a DNS lookup.
186+
type DNSDoneInfo struct {
187+
// Addrs are the IPv4 and/or IPv6 addresses found in the DNS
188+
// lookup. The contents of the slice should not be mutated.
189+
Addrs []net.IPAddr
190+
191+
// Err is any error that occurred during the DNS lookup.
192+
Err error
193+
194+
// Coalesced is whether the Addrs were shared with another
195+
// caller who was doing the same DNS lookup concurrently.
196+
Coalesced bool
197+
}
198+
199+
func (t *ClientTrace) hasNetHooks() bool {
200+
if t == nil {
201+
return false
202+
}
203+
return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
204+
}
205+
206+
// GotConnInfo is the argument to the ClientTrace.GotConn function and
207+
// contains information about the obtained connection.
208+
type GotConnInfo struct {
209+
// Conn is the connection that was obtained. It is owned by
210+
// the http.Transport and should not be read, written or
211+
// closed by users of ClientTrace.
212+
Conn net.Conn
213+
214+
// Reused is whether this connection has been previously
215+
// used for another HTTP request.
216+
Reused bool
217+
218+
// WasIdle is whether this connection was obtained from an
219+
// idle pool.
220+
WasIdle bool
221+
222+
// IdleTime reports how long the connection was previously
223+
// idle, if WasIdle is true.
224+
IdleTime time.Duration
225+
}

src/net/http/httptrace/trace_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.h
4+
5+
package httptrace
6+
7+
import (
8+
"bytes"
9+
"testing"
10+
)
11+
12+
func TestCompose(t *testing.T) {
13+
var buf bytes.Buffer
14+
var testNum int
15+
16+
connectStart := func(b byte) func(network, addr string) {
17+
return func(network, addr string) {
18+
if addr != "addr" {
19+
t.Errorf(`%d. args for %Q case = %q, %q; want addr of "addr"`, testNum, b, network, addr)
20+
}
21+
buf.WriteByte(b)
22+
}
23+
}
24+
25+
tests := [...]struct {
26+
trace, old *ClientTrace
27+
want string
28+
}{
29+
0: {
30+
want: "T",
31+
trace: &ClientTrace{
32+
ConnectStart: connectStart('T'),
33+
},
34+
},
35+
1: {
36+
want: "TO",
37+
trace: &ClientTrace{
38+
ConnectStart: connectStart('T'),
39+
},
40+
old: &ClientTrace{ConnectStart: connectStart('O')},
41+
},
42+
2: {
43+
want: "O",
44+
trace: &ClientTrace{},
45+
old: &ClientTrace{ConnectStart: connectStart('O')},
46+
},
47+
}
48+
for i, tt := range tests {
49+
testNum = i
50+
buf.Reset()
51+
52+
tr := *tt.trace
53+
tr.compose(tt.old)
54+
if tr.ConnectStart != nil {
55+
tr.ConnectStart("net", "addr")
56+
}
57+
if got := buf.String(); got != tt.want {
58+
t.Errorf("%d. got = %q; want %q", i, got, tt.want)
59+
}
60+
}
61+
62+
}

0 commit comments

Comments
 (0)