Skip to content

Commit 51e3f6d

Browse files
committed
wip use RoundTripper
1 parent bc804cc commit 51e3f6d

File tree

3 files changed

+170
-192
lines changed

3 files changed

+170
-192
lines changed

prometheus/promhttp/client.go

Lines changed: 141 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,55 +20,160 @@
2020
package promhttp
2121

2222
import (
23-
"io"
23+
"context"
24+
"crypto/tls"
2425
"net/http"
25-
"net/url"
26-
"strings"
27-
)
26+
"net/http/httptrace"
27+
"time"
2828

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+
)
3232

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
3434
// Middleware funcs, allowing the user to construct layers of middleware around
3535
// an http client request.
36-
type ClientMiddleware func(req *http.Request) (*http.Response, error)
36+
type RoundTripperFunc func(req *http.Request) (*http.Response, error)
3737

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)
4141
}
4242

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+
})
5097
}
5198

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+
// LatencyC accepts an ObserverVec interface and an http.RoundTripper,
131+
// returning a new http.RoundTripper that wraps the supplied http.RoundTripper.
132+
// The provided ObserverVec must be registered in a registry in order to be
133+
// used. The instance labels "code" and "method" are supported on the provided
134+
// ObserverVec. Note: Partitioning histograms is expensive.
135+
func LatencyC(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc {
136+
code, method := checkLabels(obs)
137+
138+
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
139+
var (
140+
start = time.Now()
141+
resp, err = next.RoundTrip(r)
142+
)
143+
if err != nil {
144+
return nil, err
145+
}
146+
obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds())
147+
return resp, err
148+
})
59149
}
60150

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)
151+
func checkEventLabel(c prometheus.Collector) {
152+
var (
153+
desc *prometheus.Desc
154+
pm dto.Metric
155+
)
156+
157+
descc := make(chan *prometheus.Desc, 1)
158+
c.Describe(descc)
159+
160+
select {
161+
case desc = <-descc:
162+
default:
163+
panic("no description provided by collector")
164+
}
165+
166+
m, err := prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, "")
64167
if err != nil {
65-
return nil, err
168+
panic("error checking metric for labels")
66169
}
67-
req.Header.Set("Content-Type", contentType)
68-
return c.Do(req)
69-
}
70170

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()))
171+
if err := m.Write(&pm); err != nil {
172+
panic("error checking metric for labels")
173+
}
174+
175+
name := *pm.Label[0].Name
176+
if name != "event" {
177+
panic("metric partitioned with non-supported label")
178+
}
74179
}

prometheus/promhttp/client_test.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,51 @@ import (
3030
)
3131

3232
func TestClientMiddlewareAPI(t *testing.T) {
33-
client := *http.DefaultClient
34-
client.Timeout = 300 * time.Millisecond
33+
client := http.DefaultClient
34+
client.Timeout = 1 * time.Second
3535

36-
inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "in_flight"})
36+
inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{
37+
Name: "in_flight",
38+
Help: "In-flight count.",
39+
})
3740

3841
counter := prometheus.NewCounterVec(
39-
prometheus.CounterOpts{Name: "test_counter"},
42+
prometheus.CounterOpts{
43+
Name: "test_counter",
44+
Help: "Counter.",
45+
},
4046
[]string{"code", "method"},
4147
)
4248

43-
histVec := prometheus.NewHistogramVec(
49+
traceVec := prometheus.NewHistogramVec(
4450
prometheus.HistogramOpts{
45-
Name: "latency",
51+
Name: "trace_latency",
52+
Help: "Trace latency histogram.",
4653
Buckets: prometheus.DefBuckets,
4754
},
4855
[]string{"event"},
4956
)
5057

51-
promclient := InFlightC(inFlightGauge,
58+
latencyVec := prometheus.NewHistogramVec(
59+
prometheus.HistogramOpts{
60+
Name: "latency",
61+
Help: "Overall latency histogram.",
62+
Buckets: prometheus.DefBuckets,
63+
},
64+
[]string{"code", "method"},
65+
)
66+
67+
prometheus.MustRegister(counter, traceVec, latencyVec, inFlightGauge)
68+
69+
client.Transport = InFlightC(inFlightGauge,
5270
CounterC(counter,
53-
ClientTrace(histVec, &client),
71+
ClientTrace(traceVec,
72+
LatencyC(latencyVec, http.DefaultTransport),
73+
),
5474
),
5575
)
5676

57-
resp, err := promclient.Get("http://google.com")
77+
resp, err := client.Get("http://google.com")
5878
if err != nil {
5979
t.Fatalf("%v", err)
6080
}

0 commit comments

Comments
 (0)