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