|
20 | 20 | package promhttp |
21 | 21 |
|
22 | 22 | import ( |
23 | | - "io" |
| 23 | + "context" |
| 24 | + "crypto/tls" |
24 | 25 | "net/http" |
25 | | - "net/url" |
26 | | - "strings" |
27 | | -) |
| 26 | + "net/http/httptrace" |
| 27 | + "time" |
28 | 28 |
|
29 | | -type doer interface { |
30 | | - Do(*http.Request) (*http.Response, error) |
31 | | -} |
| 29 | + "github.com/prometheus/client_golang/prometheus" |
| 30 | + dto "github.com/prometheus/client_model/go" |
| 31 | +) |
32 | 32 |
|
33 | | -// ClientMiddleware is an adapter to allow wrapping an http.Client or other |
| 33 | +// RoundTripperFunc is an adapter to allow wrapping an http.Client or other |
34 | 34 | // Middleware funcs, allowing the user to construct layers of middleware around |
35 | 35 | // an http client request. |
36 | | -type ClientMiddleware func(req *http.Request) (*http.Response, error) |
| 36 | +type RoundTripperFunc func(req *http.Request) (*http.Response, error) |
37 | 37 |
|
38 | | -// Do implements the httpClient interface. |
39 | | -func (c ClientMiddleware) Do(r *http.Request) (*http.Response, error) { |
40 | | - return c(r) |
| 38 | +// RoundTrip implements the RoundTripper interface. |
| 39 | +func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { |
| 40 | + return rt(r) |
41 | 41 | } |
42 | 42 |
|
43 | | -// Get implements the httpClient interface. |
44 | | -func (c ClientMiddleware) Get(url string) (*http.Response, error) { |
45 | | - req, err := http.NewRequest("GET", url, nil) |
46 | | - if err != nil { |
47 | | - return nil, err |
48 | | - } |
49 | | - return c.Do(req) |
| 43 | +// ClientTrace accepts an ObserverVec interface and a http.RoundTripper, |
| 44 | +// returning a RoundTripperFunc that wraps the supplied httpClient. The |
| 45 | +// provided ObserverVec must be registered in a registry in order to be used. |
| 46 | +// Note: Partitioning histograms is expensive. |
| 47 | +func ClientTrace(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { |
| 48 | + // The supplied ObserverVec NEEDS a label for the httptrace events. |
| 49 | + // TODO: Using `event` for now, but any other name is acceptable. |
| 50 | + |
| 51 | + checkEventLabel(obs) |
| 52 | + // TODO: Pass in struct of observers that map to the ClientTrace |
| 53 | + // functions. |
| 54 | + // Could use a vec if they want, but we only need an Observer (only |
| 55 | + // call observe, they have to apply their own labels). |
| 56 | + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { |
| 57 | + var ( |
| 58 | + start = time.Now() |
| 59 | + ) |
| 60 | + |
| 61 | + trace := &httptrace.ClientTrace{ |
| 62 | + DNSStart: func(_ httptrace.DNSStartInfo) { |
| 63 | + obs.WithLabelValues("DNSStart").Observe(time.Since(start).Seconds()) |
| 64 | + }, |
| 65 | + DNSDone: func(_ httptrace.DNSDoneInfo) { |
| 66 | + obs.WithLabelValues("DNSDone").Observe(time.Since(start).Seconds()) |
| 67 | + }, |
| 68 | + ConnectStart: func(_, _ string) { |
| 69 | + obs.WithLabelValues("ConnectStart").Observe(time.Since(start).Seconds()) |
| 70 | + }, |
| 71 | + ConnectDone: func(_, _ string, err error) { |
| 72 | + if err != nil { |
| 73 | + return |
| 74 | + } |
| 75 | + obs.WithLabelValues("ConnectDone").Observe(time.Since(start).Seconds()) |
| 76 | + }, |
| 77 | + GotFirstResponseByte: func() { |
| 78 | + obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) |
| 79 | + }, |
| 80 | + TLSHandshakeStart: func() { |
| 81 | + obs.WithLabelValues("TLSHandshakeStart").Observe(time.Since(start).Seconds()) |
| 82 | + }, |
| 83 | + TLSHandshakeDone: func(_ tls.ConnectionState, err error) { |
| 84 | + if err != nil { |
| 85 | + return |
| 86 | + } |
| 87 | + obs.WithLabelValues("TLSHandshakeDone").Observe(time.Since(start).Seconds()) |
| 88 | + }, |
| 89 | + WroteRequest: func(_ httptrace.WroteRequestInfo) { |
| 90 | + obs.WithLabelValues("WroteRequest").Observe(time.Since(start).Seconds()) |
| 91 | + }, |
| 92 | + } |
| 93 | + r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) |
| 94 | + |
| 95 | + return next.RoundTrip(r) |
| 96 | + }) |
50 | 97 | } |
51 | 98 |
|
52 | | -// Head implements the httpClient interface. |
53 | | -func (c ClientMiddleware) Head(url string) (*http.Response, error) { |
54 | | - req, err := http.NewRequest("HEAD", url, nil) |
55 | | - if err != nil { |
56 | | - return nil, err |
57 | | - } |
58 | | - return c.Do(req) |
| 99 | +// InFlightC accepts a Gauge and an http.RoundTripper, returning a new |
| 100 | +// RoundTripperFunc that wraps the supplied http.RoundTripper. The provided |
| 101 | +// Gauge must be registered in a registry in order to be used. |
| 102 | +func InFlightC(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { |
| 103 | + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { |
| 104 | + gauge.Inc() |
| 105 | + resp, err := next.RoundTrip(r) |
| 106 | + if err != nil { |
| 107 | + return nil, err |
| 108 | + } |
| 109 | + gauge.Dec() |
| 110 | + return resp, err |
| 111 | + }) |
| 112 | +} |
| 113 | + |
| 114 | +// Counter accepts an CounterVec interface and an http.RoundTripper, returning |
| 115 | +// a new RoundTripperFunc that wraps the supplied http.RoundTripper. The |
| 116 | +// provided CounterVec must be registered in a registry in order to be used. |
| 117 | +func CounterC(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc { |
| 118 | + code, method := checkLabels(counter) |
| 119 | + |
| 120 | + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { |
| 121 | + resp, err := next.RoundTrip(r) |
| 122 | + if err != nil { |
| 123 | + return nil, err |
| 124 | + } |
| 125 | + counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() |
| 126 | + return resp, err |
| 127 | + }) |
| 128 | +} |
| 129 | + |
| 130 | +func LatencyC(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { |
| 131 | + code, method := checkLabels(obs) |
| 132 | + |
| 133 | + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { |
| 134 | + var ( |
| 135 | + start = time.Now() |
| 136 | + resp, err = next.RoundTrip(r) |
| 137 | + ) |
| 138 | + if err != nil { |
| 139 | + return nil, err |
| 140 | + } |
| 141 | + obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds()) |
| 142 | + return resp, err |
| 143 | + }) |
59 | 144 | } |
60 | 145 |
|
61 | | -// Post implements the httpClient interface. |
62 | | -func (c ClientMiddleware) Post(url string, contentType string, body io.Reader) (*http.Response, error) { |
63 | | - req, err := http.NewRequest("POST", url, body) |
| 146 | +func checkEventLabel(c prometheus.Collector) { |
| 147 | + var ( |
| 148 | + desc *prometheus.Desc |
| 149 | + pm dto.Metric |
| 150 | + ) |
| 151 | + |
| 152 | + descc := make(chan *prometheus.Desc, 1) |
| 153 | + c.Describe(descc) |
| 154 | + |
| 155 | + select { |
| 156 | + case desc = <-descc: |
| 157 | + default: |
| 158 | + panic("no description provided by collector") |
| 159 | + } |
| 160 | + |
| 161 | + m, err := prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, "") |
64 | 162 | if err != nil { |
65 | | - return nil, err |
| 163 | + panic("error checking metric for labels") |
66 | 164 | } |
67 | | - req.Header.Set("Content-Type", contentType) |
68 | | - return c.Do(req) |
69 | | -} |
70 | 165 |
|
71 | | -// PostForm implements the httpClient interface. |
72 | | -func (c ClientMiddleware) PostForm(url string, data url.Values) (*http.Response, error) { |
73 | | - return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) |
| 166 | + if err := m.Write(&pm); err != nil { |
| 167 | + panic("error checking metric for labels") |
| 168 | + } |
| 169 | + |
| 170 | + name := *pm.Label[0].Name |
| 171 | + if name != "event" { |
| 172 | + panic("metric partitioned with non-supported label") |
| 173 | + } |
74 | 174 | } |
0 commit comments