metrics.go (5933B)
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 }