Skip to content

Commit 80b79e7

Browse files
committed
wip use RoundTripper
1 parent bc804cc commit 80b79e7

File tree

3 files changed

+140
-187
lines changed

3 files changed

+140
-187
lines changed

prometheus/promhttp/client.go

Lines changed: 136 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,55 +20,155 @@
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+
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+
})
59144
}
60145

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, "")
64162
if err != nil {
65-
return nil, err
163+
panic("error checking metric for labels")
66164
}
67-
req.Header.Set("Content-Type", contentType)
68-
return c.Do(req)
69-
}
70165

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+
}
74174
}

prometheus/promhttp/client_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ 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

3636
inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "in_flight"})
3737

@@ -48,9 +48,9 @@ func TestClientMiddlewareAPI(t *testing.T) {
4848
[]string{"event"},
4949
)
5050

51-
promclient := InFlightC(inFlightGauge,
51+
client.RoundTripper = InFlightC(inFlightGauge,
5252
CounterC(counter,
53-
ClientTrace(histVec, &client),
53+
ClientTrace(histVec, http.DefaultClient),
5454
),
5555
)
5656

prometheus/promhttp/middleware.go

Lines changed: 0 additions & 147 deletions
This file was deleted.

0 commit comments

Comments
 (0)