-
Notifications
You must be signed in to change notification settings - Fork 39
/
httptrace.go
134 lines (114 loc) · 4.38 KB
/
httptrace.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package servicecheck
import (
"context"
"crypto/tls"
"log/slog"
"net/http"
"net/http/httptrace"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// unique type for context.Context to avoid collisions.
type kubenurseTypeKey struct{}
// // http.RoundTripper
type RoundTripperFunc func(req *http.Request) (*http.Response, error)
func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return rt(r)
}
// This collects traces and logs errors. As promhttp.InstrumentRoundTripperTrace doesn't process
// errors, this is custom made and inspired by prometheus/client_golang's promhttp
//
//nolint:funlen // needed to pack all histograms and use them directly in the httptrace wrapper
func withHttptrace(registry *prometheus.Registry, next http.RoundTripper, durHist []float64) http.RoundTripper {
httpclientReqTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Name: "httpclient_requests_total",
Help: "A counter for requests from the kubenurse http client.",
},
[]string{"code", "method", "type"},
)
httpclientReqDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Name: "httpclient_request_duration_seconds",
Help: "A latency histogram of request latencies from the kubenurse http client.",
Buckets: durHist,
},
[]string{"type"},
)
httpclientTraceReqDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Name: "httpclient_trace_request_duration_seconds",
Help: "Latency histogram for requests from the kubenurse http client. Time in seconds since the start of the http request.",
Buckets: durHist,
},
[]string{"event", "type"},
)
errorCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Name: "errors_total",
Help: "Kubenurse error counter partitioned by error type",
},
[]string{"event", "type"},
)
registry.MustRegister(httpclientReqTotal, httpclientReqDuration, httpclientTraceReqDuration, errorCounter)
collectMetric := func(traceEventType string, start time.Time, r *http.Request, err error) {
td := time.Since(start).Seconds()
kubenurseTypeLabel := r.Context().Value(kubenurseTypeKey{}).(string)
// If we get an error inside a trace, log it
if err != nil {
errorCounter.WithLabelValues(traceEventType, kubenurseTypeLabel).Inc()
slog.Error("request failure in httptrace", "event_type", traceEventType, "request_type", kubenurseTypeLabel, "err", err)
return
}
httpclientTraceReqDuration.WithLabelValues(traceEventType, kubenurseTypeLabel).Observe(td)
}
// Return a http.RoundTripper for tracing requests
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
// Capture request time
start := time.Now()
// Add tracing hooks
trace := &httptrace.ClientTrace{
GotConn: func(_ httptrace.GotConnInfo) {
collectMetric("got_conn", start, r, nil)
},
DNSStart: func(_ httptrace.DNSStartInfo) {
collectMetric("dns_start", start, r, nil)
},
DNSDone: func(info httptrace.DNSDoneInfo) {
collectMetric("dns_done", start, r, info.Err)
},
ConnectStart: func(_, _ string) {
collectMetric("connect_start", start, r, nil)
},
ConnectDone: func(_, _ string, err error) {
collectMetric("connect_done", start, r, err)
},
TLSHandshakeStart: func() {
collectMetric("tls_handshake_start", start, r, nil)
},
TLSHandshakeDone: func(_ tls.ConnectionState, err error) {
collectMetric("tls_handshake_done", start, r, err)
},
WroteRequest: func(info httptrace.WroteRequestInfo) {
collectMetric("wrote_request", start, r, info.Err)
},
GotFirstResponseByte: func() {
collectMetric("got_first_resp_byte", start, r, nil)
},
}
// Do request with tracing enabled
r = r.WithContext(httptrace.WithClientTrace(r.Context(), trace))
typeFromCtxFn := promhttp.WithLabelFromCtx("type", func(ctx context.Context) string {
return ctx.Value(kubenurseTypeKey{}).(string)
})
rt := next // variable pinning :) essential, to prevent always re-instrumenting the original variable
rt = promhttp.InstrumentRoundTripperCounter(httpclientReqTotal, rt, typeFromCtxFn)
rt = promhttp.InstrumentRoundTripperDuration(httpclientReqDuration, rt, typeFromCtxFn)
return rt.RoundTrip(r)
})
}