github.com/Kolosok86/http@v0.1.2/httptrace/trace.go (about)

     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  	"net"
    12  	"reflect"
    13  	"time"
    14  
    15  	"github.com/Kolosok86/http/internal/nettrace"
    16  	"github.com/Kolosok86/http/textproto"
    17  	tls "github.com/refraction-networking/utls"
    18  )
    19  
    20  // unique type to prevent assignment.
    21  type clientEventContextKey struct{}
    22  
    23  // ContextClientTrace returns the ClientTrace associated with the
    24  // provided context. If none, it returns nil.
    25  func ContextClientTrace(ctx context.Context) *ClientTrace {
    26  	trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
    27  	return trace
    28  }
    29  
    30  // WithClientTrace returns a new context based on the provided parent
    31  // ctx. HTTP client requests made with the returned context will use
    32  // the provided trace hooks, in addition to any previous hooks
    33  // registered with ctx. Any hooks defined in the provided trace will
    34  // be called first.
    35  func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
    36  	if trace == nil {
    37  		panic("nil trace")
    38  	}
    39  	old := ContextClientTrace(ctx)
    40  	trace.compose(old)
    41  
    42  	ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
    43  	if trace.hasNetHooks() {
    44  		nt := &nettrace.Trace{
    45  			ConnectStart: trace.ConnectStart,
    46  			ConnectDone:  trace.ConnectDone,
    47  		}
    48  		if trace.DNSStart != nil {
    49  			nt.DNSStart = func(name string) {
    50  				trace.DNSStart(DNSStartInfo{Host: name})
    51  			}
    52  		}
    53  		if trace.DNSDone != nil {
    54  			nt.DNSDone = func(netIPs []any, coalesced bool, err error) {
    55  				addrs := make([]net.IPAddr, len(netIPs))
    56  				for i, ip := range netIPs {
    57  					addrs[i] = ip.(net.IPAddr)
    58  				}
    59  				trace.DNSDone(DNSDoneInfo{
    60  					Addrs:     addrs,
    61  					Coalesced: coalesced,
    62  					Err:       err,
    63  				})
    64  			}
    65  		}
    66  		ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
    67  	}
    68  	return ctx
    69  }
    70  
    71  // ClientTrace is a set of hooks to run at various stages of an outgoing
    72  // HTTP request. Any particular hook may be nil. Functions may be
    73  // called concurrently from different goroutines and some may be called
    74  // after the request has completed or failed.
    75  //
    76  // ClientTrace currently traces a single HTTP request & response
    77  // during a single round trip and has no hooks that span a series
    78  // of redirected requests.
    79  //
    80  // See https://blog.golang.org/http-tracing for more.
    81  type ClientTrace struct {
    82  	// GetConn is called before a connection is created or
    83  	// retrieved from an idle pool. The hostPort is the
    84  	// "host:port" of the target or proxy. GetConn is called even
    85  	// if there's already an idle cached connection available.
    86  	GetConn func(hostPort string)
    87  
    88  	// GotConn is called after a successful connection is
    89  	// obtained. There is no hook for failure to obtain a
    90  	// connection; instead, use the error from
    91  	// Transport.RoundTrip.
    92  	GotConn func(GotConnInfo)
    93  
    94  	// PutIdleConn is called when the connection is returned to
    95  	// the idle pool. If err is nil, the connection was
    96  	// successfully returned to the idle pool. If err is non-nil,
    97  	// it describes why not. PutIdleConn is not called if
    98  	// connection reuse is disabled via Transport.DisableKeepAlives.
    99  	// PutIdleConn is called before the caller's Response.Body.Close
   100  	// call returns.
   101  	// For HTTP/2, this hook is not currently used.
   102  	PutIdleConn func(err error)
   103  
   104  	// GotFirstResponseByte is called when the first byte of the response
   105  	// headers is available.
   106  	GotFirstResponseByte func()
   107  
   108  	// Got100Continue is called if the server replies with a "100
   109  	// Continue" response.
   110  	Got100Continue func()
   111  
   112  	// Got1xxResponse is called for each 1xx informational response header
   113  	// returned before the final non-1xx response. Got1xxResponse is called
   114  	// for "100 Continue" responses, even if Got100Continue is also defined.
   115  	// If it returns an error, the client request is aborted with that error value.
   116  	Got1xxResponse func(code int, header textproto.MIMEHeader) error
   117  
   118  	// DNSStart is called when a DNS lookup begins.
   119  	DNSStart func(DNSStartInfo)
   120  
   121  	// DNSDone is called when a DNS lookup ends.
   122  	DNSDone func(DNSDoneInfo)
   123  
   124  	// ConnectStart is called when a new connection's Dial begins.
   125  	// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
   126  	// enabled, this may be called multiple times.
   127  	ConnectStart func(network, addr string)
   128  
   129  	// ConnectDone is called when a new connection's Dial
   130  	// completes. The provided err indicates whether the
   131  	// connection completed successfully.
   132  	// If net.Dialer.DualStack ("Happy Eyeballs") support is
   133  	// enabled, this may be called multiple times.
   134  	ConnectDone func(network, addr string, err error)
   135  
   136  	// TLSHandshakeStart is called when the TLS handshake is started. When
   137  	// connecting to an HTTPS site via an HTTP proxy, the handshake happens
   138  	// after the CONNECT request is processed by the proxy.
   139  	TLSHandshakeStart func()
   140  
   141  	// TLSHandshakeDone is called after the TLS handshake with either the
   142  	// successful handshake's connection state, or a non-nil error on handshake
   143  	// failure.
   144  	TLSHandshakeDone func(tls.ConnectionState, error)
   145  
   146  	// WroteHeaderField is called after the Transport has written
   147  	// each request header. At the time of this call the values
   148  	// might be buffered and not yet written to the network.
   149  	WroteHeaderField func(key string, value []string)
   150  
   151  	// WroteHeaders is called after the Transport has written
   152  	// all request headers.
   153  	WroteHeaders func()
   154  
   155  	// Wait100Continue is called if the Request specified
   156  	// "Expect: 100-continue" and the Transport has written the
   157  	// request headers but is waiting for "100 Continue" from the
   158  	// server before writing the request body.
   159  	Wait100Continue func()
   160  
   161  	// WroteRequest is called with the result of writing the
   162  	// request and any body. It may be called multiple times
   163  	// in the case of retried requests.
   164  	WroteRequest func(WroteRequestInfo)
   165  }
   166  
   167  // WroteRequestInfo contains information provided to the WroteRequest
   168  // hook.
   169  type WroteRequestInfo struct {
   170  	// Err is any error encountered while writing the Request.
   171  	Err error
   172  }
   173  
   174  // compose modifies t such that it respects the previously-registered hooks in old,
   175  // subject to the composition policy requested in t.Compose.
   176  func (t *ClientTrace) compose(old *ClientTrace) {
   177  	if old == nil {
   178  		return
   179  	}
   180  	tv := reflect.ValueOf(t).Elem()
   181  	ov := reflect.ValueOf(old).Elem()
   182  	structType := tv.Type()
   183  	for i := 0; i < structType.NumField(); i++ {
   184  		tf := tv.Field(i)
   185  		hookType := tf.Type()
   186  		if hookType.Kind() != reflect.Func {
   187  			continue
   188  		}
   189  		of := ov.Field(i)
   190  		if of.IsNil() {
   191  			continue
   192  		}
   193  		if tf.IsNil() {
   194  			tf.Set(of)
   195  			continue
   196  		}
   197  
   198  		// Make a copy of tf for tf to call. (Otherwise it
   199  		// creates a recursive call cycle and stack overflows)
   200  		tfCopy := reflect.ValueOf(tf.Interface())
   201  
   202  		// We need to call both tf and of in some order.
   203  		newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value {
   204  			tfCopy.Call(args)
   205  			return of.Call(args)
   206  		})
   207  		tv.Field(i).Set(newFunc)
   208  	}
   209  }
   210  
   211  // DNSStartInfo contains information about a DNS request.
   212  type DNSStartInfo struct {
   213  	Host string
   214  }
   215  
   216  // DNSDoneInfo contains information about the results of a DNS lookup.
   217  type DNSDoneInfo struct {
   218  	// Addrs are the IPv4 and/or IPv6 addresses found in the DNS
   219  	// lookup. The contents of the slice should not be mutated.
   220  	Addrs []net.IPAddr
   221  
   222  	// Err is any error that occurred during the DNS lookup.
   223  	Err error
   224  
   225  	// Coalesced is whether the Addrs were shared with another
   226  	// caller who was doing the same DNS lookup concurrently.
   227  	Coalesced bool
   228  }
   229  
   230  func (t *ClientTrace) hasNetHooks() bool {
   231  	if t == nil {
   232  		return false
   233  	}
   234  	return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
   235  }
   236  
   237  // GotConnInfo is the argument to the ClientTrace.GotConn function and
   238  // contains information about the obtained connection.
   239  type GotConnInfo struct {
   240  	// Conn is the connection that was obtained. It is owned by
   241  	// the http.Transport and should not be read, written or
   242  	// closed by users of ClientTrace.
   243  	Conn net.Conn
   244  
   245  	// Reused is whether this connection has been previously
   246  	// used for another HTTP request.
   247  	Reused bool
   248  
   249  	// WasIdle is whether this connection was obtained from an
   250  	// idle pool.
   251  	WasIdle bool
   252  
   253  	// IdleTime reports how long the connection was previously
   254  	// idle, if WasIdle is true.
   255  	IdleTime time.Duration
   256  }