metrics.go raw

   1  package http
   2  
   3  import (
   4  	"context"
   5  	"crypto/tls"
   6  	"net/http"
   7  	"net/http/httptrace"
   8  	"sync/atomic"
   9  	"time"
  10  
  11  	"github.com/aws/smithy-go/metrics"
  12  )
  13  
  14  var now = time.Now
  15  
  16  // withMetrics instruments an HTTP client and context to collect HTTP metrics.
  17  func withMetrics(parent context.Context, client ClientDo, meter metrics.Meter) (
  18  	context.Context, ClientDo, error,
  19  ) {
  20  	// WithClientTrace is an expensive operation - avoid calling it if we're
  21  	// not actually using a metrics sink.
  22  	if _, ok := meter.(metrics.NopMeter); ok {
  23  		return parent, client, nil
  24  	}
  25  
  26  	hm, err := newHTTPMetrics(meter)
  27  	if err != nil {
  28  		return nil, nil, err
  29  	}
  30  
  31  	ctx := httptrace.WithClientTrace(parent, &httptrace.ClientTrace{
  32  		DNSStart:          hm.DNSStart,
  33  		ConnectStart:      hm.ConnectStart,
  34  		TLSHandshakeStart: hm.TLSHandshakeStart,
  35  
  36  		GotConn:              hm.GotConn(parent),
  37  		PutIdleConn:          hm.PutIdleConn(parent),
  38  		ConnectDone:          hm.ConnectDone(parent),
  39  		DNSDone:              hm.DNSDone(parent),
  40  		TLSHandshakeDone:     hm.TLSHandshakeDone(parent),
  41  		GotFirstResponseByte: hm.GotFirstResponseByte(parent),
  42  	})
  43  	return ctx, &timedClientDo{client, hm}, nil
  44  }
  45  
  46  type timedClientDo struct {
  47  	ClientDo
  48  	hm *httpMetrics
  49  }
  50  
  51  func (c *timedClientDo) Do(r *http.Request) (*http.Response, error) {
  52  	c.hm.doStart.Store(now())
  53  	resp, err := c.ClientDo.Do(r)
  54  
  55  	c.hm.DoRequestDuration.Record(r.Context(), c.hm.doStart.Elapsed())
  56  	return resp, err
  57  }
  58  
  59  type httpMetrics struct {
  60  	DNSLookupDuration    metrics.Float64Histogram   // client.http.connections.dns_lookup_duration
  61  	ConnectDuration      metrics.Float64Histogram   // client.http.connections.acquire_duration
  62  	TLSHandshakeDuration metrics.Float64Histogram   // client.http.connections.tls_handshake_duration
  63  	ConnectionUsage      metrics.Int64UpDownCounter // client.http.connections.usage
  64  
  65  	DoRequestDuration metrics.Float64Histogram // client.http.do_request_duration
  66  	TimeToFirstByte   metrics.Float64Histogram // client.http.time_to_first_byte
  67  
  68  	doStart      safeTime
  69  	dnsStart     safeTime
  70  	connectStart safeTime
  71  	tlsStart     safeTime
  72  }
  73  
  74  func newHTTPMetrics(meter metrics.Meter) (*httpMetrics, error) {
  75  	hm := &httpMetrics{}
  76  
  77  	var err error
  78  	hm.DNSLookupDuration, err = meter.Float64Histogram("client.http.connections.dns_lookup_duration", func(o *metrics.InstrumentOptions) {
  79  		o.UnitLabel = "s"
  80  		o.Description = "The time it takes a request to perform DNS lookup."
  81  	})
  82  	if err != nil {
  83  		return nil, err
  84  	}
  85  	hm.ConnectDuration, err = meter.Float64Histogram("client.http.connections.acquire_duration", func(o *metrics.InstrumentOptions) {
  86  		o.UnitLabel = "s"
  87  		o.Description = "The time it takes a request to acquire a connection."
  88  	})
  89  	if err != nil {
  90  		return nil, err
  91  	}
  92  	hm.TLSHandshakeDuration, err = meter.Float64Histogram("client.http.connections.tls_handshake_duration", func(o *metrics.InstrumentOptions) {
  93  		o.UnitLabel = "s"
  94  		o.Description = "The time it takes an HTTP request to perform the TLS handshake."
  95  	})
  96  	if err != nil {
  97  		return nil, err
  98  	}
  99  	hm.ConnectionUsage, err = meter.Int64UpDownCounter("client.http.connections.usage", func(o *metrics.InstrumentOptions) {
 100  		o.UnitLabel = "{connection}"
 101  		o.Description = "Current state of connections pool."
 102  	})
 103  	if err != nil {
 104  		return nil, err
 105  	}
 106  	hm.DoRequestDuration, err = meter.Float64Histogram("client.http.do_request_duration", func(o *metrics.InstrumentOptions) {
 107  		o.UnitLabel = "s"
 108  		o.Description = "Time spent performing an entire HTTP transaction."
 109  	})
 110  	if err != nil {
 111  		return nil, err
 112  	}
 113  	hm.TimeToFirstByte, err = meter.Float64Histogram("client.http.time_to_first_byte", func(o *metrics.InstrumentOptions) {
 114  		o.UnitLabel = "s"
 115  		o.Description = "Time from start of transaction to when the first response byte is available."
 116  	})
 117  	if err != nil {
 118  		return nil, err
 119  	}
 120  
 121  	return hm, nil
 122  }
 123  
 124  func (m *httpMetrics) DNSStart(httptrace.DNSStartInfo) {
 125  	m.dnsStart.Store(now())
 126  }
 127  
 128  func (m *httpMetrics) ConnectStart(string, string) {
 129  	m.connectStart.Store(now())
 130  }
 131  
 132  func (m *httpMetrics) TLSHandshakeStart() {
 133  	m.tlsStart.Store(now())
 134  }
 135  
 136  func (m *httpMetrics) GotConn(ctx context.Context) func(httptrace.GotConnInfo) {
 137  	return func(httptrace.GotConnInfo) {
 138  		m.addConnAcquired(ctx, 1)
 139  	}
 140  }
 141  
 142  func (m *httpMetrics) PutIdleConn(ctx context.Context) func(error) {
 143  	return func(error) {
 144  		m.addConnAcquired(ctx, -1)
 145  	}
 146  }
 147  
 148  func (m *httpMetrics) DNSDone(ctx context.Context) func(httptrace.DNSDoneInfo) {
 149  	return func(httptrace.DNSDoneInfo) {
 150  		m.DNSLookupDuration.Record(ctx, m.dnsStart.Elapsed())
 151  	}
 152  }
 153  
 154  func (m *httpMetrics) ConnectDone(ctx context.Context) func(string, string, error) {
 155  	return func(string, string, error) {
 156  		m.ConnectDuration.Record(ctx, m.connectStart.Elapsed())
 157  	}
 158  }
 159  
 160  func (m *httpMetrics) TLSHandshakeDone(ctx context.Context) func(tls.ConnectionState, error) {
 161  	return func(tls.ConnectionState, error) {
 162  		m.TLSHandshakeDuration.Record(ctx, m.tlsStart.Elapsed())
 163  	}
 164  }
 165  
 166  func (m *httpMetrics) GotFirstResponseByte(ctx context.Context) func() {
 167  	return func() {
 168  		m.TimeToFirstByte.Record(ctx, m.doStart.Elapsed())
 169  	}
 170  }
 171  
 172  func (m *httpMetrics) addConnAcquired(ctx context.Context, incr int64) {
 173  	m.ConnectionUsage.Add(ctx, incr, func(o *metrics.RecordMetricOptions) {
 174  		o.Properties.Set("state", "acquired")
 175  	})
 176  }
 177  
 178  // Not used: it is recommended to track acquired vs idle conn, but we can't
 179  // determine when something is truly idle with the current HTTP client hooks
 180  // available to us.
 181  func (m *httpMetrics) addConnIdle(ctx context.Context, incr int64) {
 182  	m.ConnectionUsage.Add(ctx, incr, func(o *metrics.RecordMetricOptions) {
 183  		o.Properties.Set("state", "idle")
 184  	})
 185  }
 186  
 187  type safeTime struct {
 188  	atomic.Value // time.Time
 189  }
 190  
 191  func (st *safeTime) Store(v time.Time) {
 192  	st.Value.Store(v)
 193  }
 194  
 195  func (st *safeTime) Load() time.Time {
 196  	t, _ := st.Value.Load().(time.Time)
 197  	return t
 198  }
 199  
 200  func (st *safeTime) Elapsed() float64 {
 201  	end := now()
 202  	elapsed := end.Sub(st.Load())
 203  	return float64(elapsed) / 1e9
 204  }
 205