|
| 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 | +} |
0 commit comments