trace.mx raw

   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 httptrace provides mechanisms to trace the events within
   6  // HTTP client requests.
   7  package httptrace
   8  
   9  import (
  10  	"context"
  11  	"crypto/tls"
  12  	"internal/nettrace"
  13  	"net"
  14  	"net/textproto"
  15  	"time"
  16  )
  17  
  18  // unique type to prevent assignment.
  19  type clientEventContextKey struct{}
  20  
  21  // ContextClientTrace returns the [ClientTrace] associated with the
  22  // provided context. If none, it returns nil.
  23  func ContextClientTrace(ctx context.Context) *ClientTrace {
  24  	trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
  25  	return trace
  26  }
  27  
  28  // WithClientTrace returns a new context based on the provided parent
  29  // ctx. HTTP client requests made with the returned context will use
  30  // the provided trace hooks, in addition to any previous hooks
  31  // registered with ctx. Any hooks defined in the provided trace will
  32  // be called first.
  33  func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
  34  	if trace == nil {
  35  		panic("nil trace")
  36  	}
  37  	old := ContextClientTrace(ctx)
  38  	trace.compose(old)
  39  
  40  	ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
  41  	if trace.hasNetHooks() {
  42  		nt := &nettrace.Trace{
  43  			ConnectStart: trace.ConnectStart,
  44  			ConnectDone:  trace.ConnectDone,
  45  		}
  46  		if trace.DNSStart != nil {
  47  			nt.DNSStart = func(name []byte) {
  48  				trace.DNSStart(DNSStartInfo{Host: name})
  49  			}
  50  		}
  51  		if trace.DNSDone != nil {
  52  			nt.DNSDone = func(netIPs []any, coalesced bool, err error) {
  53  				addrs := []net.IPAddr{:len(netIPs)}
  54  				for i, ip := range netIPs {
  55  					addrs[i] = ip.(net.IPAddr)
  56  				}
  57  				trace.DNSDone(DNSDoneInfo{
  58  					Addrs:     addrs,
  59  					Coalesced: coalesced,
  60  					Err:       err,
  61  				})
  62  			}
  63  		}
  64  		ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
  65  	}
  66  	return ctx
  67  }
  68  
  69  // ClientTrace is a set of hooks to run at various stages of an outgoing
  70  // HTTP request. Any particular hook may be nil. Functions may be
  71  // called concurrently from different goroutines and some may be called
  72  // after the request has completed or failed.
  73  //
  74  // ClientTrace currently traces a single HTTP request & response
  75  // during a single round trip and has no hooks that span a series
  76  // of redirected requests.
  77  //
  78  // See https://blog.golang.org/http-tracing for more.
  79  type ClientTrace struct {
  80  	// GetConn is called before a connection is created or
  81  	// retrieved from an idle pool. The hostPort is the
  82  	// "host:port" of the target or proxy. GetConn is called even
  83  	// if there's already an idle cached connection available.
  84  	GetConn func(hostPort string)
  85  
  86  	// GotConn is called after a successful connection is
  87  	// obtained. There is no hook for failure to obtain a
  88  	// connection; instead, use the error from
  89  	// Transport.RoundTrip.
  90  	GotConn func(GotConnInfo)
  91  
  92  	// PutIdleConn is called when the connection is returned to
  93  	// the idle pool. If err is nil, the connection was
  94  	// successfully returned to the idle pool. If err is non-nil,
  95  	// it describes why not. PutIdleConn is not called if
  96  	// connection reuse is disabled via Transport.DisableKeepAlives.
  97  	// PutIdleConn is called before the caller's Response.Body.Close
  98  	// call returns.
  99  	// For HTTP/2, this hook is not currently used.
 100  	PutIdleConn func(err error)
 101  
 102  	// GotFirstResponseByte is called when the first byte of the response
 103  	// headers is available.
 104  	GotFirstResponseByte func()
 105  
 106  	// Got100Continue is called if the server replies with a "100
 107  	// Continue" response.
 108  	Got100Continue func()
 109  
 110  	// Got1xxResponse is called for each 1xx informational response header
 111  	// returned before the final non-1xx response. Got1xxResponse is called
 112  	// for "100 Continue" responses, even if Got100Continue is also defined.
 113  	// If it returns an error, the client request is aborted with that error value.
 114  	Got1xxResponse func(code int, header textproto.MIMEHeader) error
 115  
 116  	// DNSStart is called when a DNS lookup begins.
 117  	DNSStart func(DNSStartInfo)
 118  
 119  	// DNSDone is called when a DNS lookup ends.
 120  	DNSDone func(DNSDoneInfo)
 121  
 122  	// ConnectStart is called when a new connection's Dial begins.
 123  	// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
 124  	// enabled, this may be called multiple times.
 125  	ConnectStart func(network, addr []byte)
 126  
 127  	// ConnectDone is called when a new connection's Dial
 128  	// completes. The provided err indicates whether the
 129  	// connection completed successfully.
 130  	// If net.Dialer.DualStack ("Happy Eyeballs") support is
 131  	// enabled, this may be called multiple times.
 132  	ConnectDone func(network, addr []byte, err error)
 133  
 134  	// TLSHandshakeStart is called when the TLS handshake is started. When
 135  	// connecting to an HTTPS site via an HTTP proxy, the handshake happens
 136  	// after the CONNECT request is processed by the proxy.
 137  	TLSHandshakeStart func()
 138  
 139  	// TLSHandshakeDone is called after the TLS handshake with either the
 140  	// successful handshake's connection state, or a non-nil error on handshake
 141  	// failure.
 142  	TLSHandshakeDone func(tls.ConnectionState, error)
 143  
 144  	// WroteHeaderField is called after the Transport has written
 145  	// each request header. At the time of this call the values
 146  	// might be buffered and not yet written to the network.
 147  	WroteHeaderField func(key string, value [][]byte)
 148  
 149  	// WroteHeaders is called after the Transport has written
 150  	// all request headers.
 151  	WroteHeaders func()
 152  
 153  	// Wait100Continue is called if the Request specified
 154  	// "Expect: 100-continue" and the Transport has written the
 155  	// request headers but is waiting for "100 Continue" from the
 156  	// server before writing the request body.
 157  	Wait100Continue func()
 158  
 159  	// WroteRequest is called with the result of writing the
 160  	// request and any body. It may be called multiple times
 161  	// in the case of retried requests.
 162  	WroteRequest func(WroteRequestInfo)
 163  }
 164  
 165  // WroteRequestInfo contains information provided to the WroteRequest
 166  // hook.
 167  type WroteRequestInfo struct {
 168  	// Err is any error encountered while writing the Request.
 169  	Err error
 170  }
 171  
 172  // compose modifies t such that it respects the previously-registered hooks in old.
 173  func (t *ClientTrace) compose(old *ClientTrace) {
 174  	if old == nil {
 175  		return
 176  	}
 177  	if old.GetConn != nil {
 178  		if prev, cur := old.GetConn, t.GetConn; cur != nil {
 179  			t.GetConn = func(hostPort string) { cur(hostPort); prev(hostPort) }
 180  		} else {
 181  			t.GetConn = prev
 182  		}
 183  	}
 184  	if old.GotConn != nil {
 185  		if prev, cur := old.GotConn, t.GotConn; cur != nil {
 186  			t.GotConn = func(info GotConnInfo) { cur(info); prev(info) }
 187  		} else {
 188  			t.GotConn = prev
 189  		}
 190  	}
 191  	if old.PutIdleConn != nil {
 192  		if prev, cur := old.PutIdleConn, t.PutIdleConn; cur != nil {
 193  			t.PutIdleConn = func(err error) { cur(err); prev(err) }
 194  		} else {
 195  			t.PutIdleConn = prev
 196  		}
 197  	}
 198  	if old.GotFirstResponseByte != nil {
 199  		if prev, cur := old.GotFirstResponseByte, t.GotFirstResponseByte; cur != nil {
 200  			t.GotFirstResponseByte = func() { cur(); prev() }
 201  		} else {
 202  			t.GotFirstResponseByte = prev
 203  		}
 204  	}
 205  	if old.Got100Continue != nil {
 206  		if prev, cur := old.Got100Continue, t.Got100Continue; cur != nil {
 207  			t.Got100Continue = func() { cur(); prev() }
 208  		} else {
 209  			t.Got100Continue = prev
 210  		}
 211  	}
 212  	if old.Got1xxResponse != nil {
 213  		if prev, cur := old.Got1xxResponse, t.Got1xxResponse; cur != nil {
 214  			t.Got1xxResponse = func(code int, header textproto.MIMEHeader) error { cur(code, header); return prev(code, header) }
 215  		} else {
 216  			t.Got1xxResponse = prev
 217  		}
 218  	}
 219  	if old.DNSStart != nil {
 220  		if prev, cur := old.DNSStart, t.DNSStart; cur != nil {
 221  			t.DNSStart = func(info DNSStartInfo) { cur(info); prev(info) }
 222  		} else {
 223  			t.DNSStart = prev
 224  		}
 225  	}
 226  	if old.DNSDone != nil {
 227  		if prev, cur := old.DNSDone, t.DNSDone; cur != nil {
 228  			t.DNSDone = func(info DNSDoneInfo) { cur(info); prev(info) }
 229  		} else {
 230  			t.DNSDone = prev
 231  		}
 232  	}
 233  	if old.ConnectStart != nil {
 234  		if prev, cur := old.ConnectStart, t.ConnectStart; cur != nil {
 235  			t.ConnectStart = func(network, addr []byte) { cur(network, addr); prev(network, addr) }
 236  		} else {
 237  			t.ConnectStart = prev
 238  		}
 239  	}
 240  	if old.ConnectDone != nil {
 241  		if prev, cur := old.ConnectDone, t.ConnectDone; cur != nil {
 242  			t.ConnectDone = func(network, addr []byte, err error) { cur(network, addr, err); prev(network, addr, err) }
 243  		} else {
 244  			t.ConnectDone = prev
 245  		}
 246  	}
 247  	if old.TLSHandshakeStart != nil {
 248  		if prev, cur := old.TLSHandshakeStart, t.TLSHandshakeStart; cur != nil {
 249  			t.TLSHandshakeStart = func() { cur(); prev() }
 250  		} else {
 251  			t.TLSHandshakeStart = prev
 252  		}
 253  	}
 254  	if old.TLSHandshakeDone != nil {
 255  		if prev, cur := old.TLSHandshakeDone, t.TLSHandshakeDone; cur != nil {
 256  			t.TLSHandshakeDone = func(cs tls.ConnectionState, err error) { cur(cs, err); prev(cs, err) }
 257  		} else {
 258  			t.TLSHandshakeDone = prev
 259  		}
 260  	}
 261  	if old.WroteHeaderField != nil {
 262  		if prev, cur := old.WroteHeaderField, t.WroteHeaderField; cur != nil {
 263  			t.WroteHeaderField = func(key string, value [][]byte) { cur(key, value); prev(key, value) }
 264  		} else {
 265  			t.WroteHeaderField = prev
 266  		}
 267  	}
 268  	if old.WroteHeaders != nil {
 269  		if prev, cur := old.WroteHeaders, t.WroteHeaders; cur != nil {
 270  			t.WroteHeaders = func() { cur(); prev() }
 271  		} else {
 272  			t.WroteHeaders = prev
 273  		}
 274  	}
 275  	if old.Wait100Continue != nil {
 276  		if prev, cur := old.Wait100Continue, t.Wait100Continue; cur != nil {
 277  			t.Wait100Continue = func() { cur(); prev() }
 278  		} else {
 279  			t.Wait100Continue = prev
 280  		}
 281  	}
 282  	if old.WroteRequest != nil {
 283  		if prev, cur := old.WroteRequest, t.WroteRequest; cur != nil {
 284  			t.WroteRequest = func(info WroteRequestInfo) { cur(info); prev(info) }
 285  		} else {
 286  			t.WroteRequest = prev
 287  		}
 288  	}
 289  }
 290  
 291  // DNSStartInfo contains information about a DNS request.
 292  type DNSStartInfo struct {
 293  	Host string
 294  }
 295  
 296  // DNSDoneInfo contains information about the results of a DNS lookup.
 297  type DNSDoneInfo struct {
 298  	// Addrs are the IPv4 and/or IPv6 addresses found in the DNS
 299  	// lookup. The contents of the slice should not be mutated.
 300  	Addrs []net.IPAddr
 301  
 302  	// Err is any error that occurred during the DNS lookup.
 303  	Err error
 304  
 305  	// Coalesced is whether the Addrs were shared with another
 306  	// caller who was doing the same DNS lookup concurrently.
 307  	Coalesced bool
 308  }
 309  
 310  func (t *ClientTrace) hasNetHooks() bool {
 311  	if t == nil {
 312  		return false
 313  	}
 314  	return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
 315  }
 316  
 317  // GotConnInfo is the argument to the [ClientTrace.GotConn] function and
 318  // contains information about the obtained connection.
 319  type GotConnInfo struct {
 320  	// Conn is the connection that was obtained. It is owned by
 321  	// the http.Transport and should not be read, written or
 322  	// closed by users of ClientTrace.
 323  	Conn net.Conn
 324  
 325  	// Reused is whether this connection has been previously
 326  	// used for another HTTP request.
 327  	Reused bool
 328  
 329  	// WasIdle is whether this connection was obtained from an
 330  	// idle pool.
 331  	WasIdle bool
 332  
 333  	// IdleTime reports how long the connection was previously
 334  	// idle, if WasIdle is true.
 335  	IdleTime time.Duration
 336  }
 337