Skip to content

Commit c7488be

Browse files
authored
Added exemplar support to http middleware. (#1055)
* Added exemplar support to http middlewares. Signed-off-by: Bartlomiej Plotka <[email protected]> * Small fix. Signed-off-by: Bartlomiej Plotka <[email protected]> * Fixed test. Signed-off-by: Bartlomiej Plotka <[email protected]> * Added tests and options for RT. Signed-off-by: bwplotka <[email protected]> * goimports. Signed-off-by: bwplotka <[email protected]>
1 parent 3faf3ba commit c7488be

File tree

5 files changed

+274
-76
lines changed

5 files changed

+274
-76
lines changed

prometheus/promhttp/instrument_client.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
3838
//
3939
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
4040
func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc {
41-
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
41+
return func(r *http.Request) (*http.Response, error) {
4242
gauge.Inc()
4343
defer gauge.Dec()
4444
return next.RoundTrip(r)
45-
})
45+
}
4646
}
4747

4848
// InstrumentRoundTripperCounter is a middleware that wraps the provided
@@ -59,22 +59,29 @@ func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripp
5959
// If the wrapped RoundTripper panics or returns a non-nil error, the Counter
6060
// is not incremented.
6161
//
62+
// Use with WithExemplarFromContext to instrument the exemplars on the counter of requests.
63+
//
6264
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
6365
func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.RoundTripper, opts ...Option) RoundTripperFunc {
64-
rtOpts := &option{}
66+
rtOpts := defaultOptions()
6567
for _, o := range opts {
66-
o(rtOpts)
68+
o.apply(rtOpts)
6769
}
6870

6971
code, method := checkLabels(counter)
7072

71-
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
73+
return func(r *http.Request) (*http.Response, error) {
7274
resp, err := next.RoundTrip(r)
7375
if err == nil {
76+
exemplarAdd(
77+
counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)),
78+
1,
79+
rtOpts.getExemplarFn(r.Context()),
80+
)
7481
counter.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)).Inc()
7582
}
7683
return resp, err
77-
})
84+
}
7885
}
7986

8087
// InstrumentRoundTripperDuration is a middleware that wraps the provided
@@ -94,24 +101,30 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou
94101
// If the wrapped RoundTripper panics or returns a non-nil error, no values are
95102
// reported.
96103
//
104+
// Use with WithExemplarFromContext to instrument the exemplars on the duration histograms.
105+
//
97106
// Note that this method is only guaranteed to never observe negative durations
98107
// if used with Go1.9+.
99108
func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundTripper, opts ...Option) RoundTripperFunc {
100-
rtOpts := &option{}
109+
rtOpts := defaultOptions()
101110
for _, o := range opts {
102-
o(rtOpts)
111+
o.apply(rtOpts)
103112
}
104113

105114
code, method := checkLabels(obs)
106115

107-
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
116+
return func(r *http.Request) (*http.Response, error) {
108117
start := time.Now()
109118
resp, err := next.RoundTrip(r)
110119
if err == nil {
111-
obs.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)).Observe(time.Since(start).Seconds())
120+
exemplarObserve(
121+
obs.With(labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)),
122+
time.Since(start).Seconds(),
123+
rtOpts.getExemplarFn(r.Context()),
124+
)
112125
}
113126
return resp, err
114-
})
127+
}
115128
}
116129

117130
// InstrumentTrace is used to offer flexibility in instrumenting the available
@@ -149,7 +162,7 @@ type InstrumentTrace struct {
149162
//
150163
// See the example for ExampleInstrumentRoundTripperDuration for example usage.
151164
func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc {
152-
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
165+
return func(r *http.Request) (*http.Response, error) {
153166
start := time.Now()
154167

155168
trace := &httptrace.ClientTrace{
@@ -231,5 +244,5 @@ func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) Ro
231244
r = r.WithContext(httptrace.WithClientTrace(r.Context(), trace))
232245

233246
return next.RoundTrip(r)
234-
})
247+
}
235248
}

prometheus/promhttp/instrument_client_test.go

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ import (
1818
"log"
1919
"net/http"
2020
"net/http/httptest"
21+
"reflect"
22+
"sort"
2123
"strings"
2224
"testing"
2325
"time"
2426

2527
"github.com/prometheus/client_golang/prometheus"
28+
29+
dto "github.com/prometheus/client_model/go"
30+
"google.golang.org/protobuf/proto"
2631
)
2732

28-
func makeInstrumentedClient() (*http.Client, *prometheus.Registry) {
33+
func makeInstrumentedClient(opts ...Option) (*http.Client, *prometheus.Registry) {
2934
client := http.DefaultClient
3035
client.Timeout = 1 * time.Second
3136

@@ -91,13 +96,91 @@ func makeInstrumentedClient() (*http.Client, *prometheus.Registry) {
9196
client.Transport = InstrumentRoundTripperInFlight(inFlightGauge,
9297
InstrumentRoundTripperCounter(counter,
9398
InstrumentRoundTripperTrace(trace,
94-
InstrumentRoundTripperDuration(histVec, http.DefaultTransport),
99+
InstrumentRoundTripperDuration(histVec, http.DefaultTransport, opts...),
95100
),
96-
),
101+
opts...),
97102
)
98103
return client, reg
99104
}
100105

106+
func labelsToLabelPair(l prometheus.Labels) []*dto.LabelPair {
107+
ret := make([]*dto.LabelPair, 0, len(l))
108+
for k, v := range l {
109+
ret = append(ret, &dto.LabelPair{Name: proto.String(k), Value: proto.String(v)})
110+
}
111+
sort.Slice(ret, func(i, j int) bool {
112+
return *ret[i].Name < *ret[j].Name
113+
})
114+
return ret
115+
}
116+
117+
func assetMetricAndExemplars(
118+
t *testing.T,
119+
reg *prometheus.Registry,
120+
expectedNumMetrics int,
121+
expectedExemplar []*dto.LabelPair,
122+
) {
123+
t.Helper()
124+
125+
mfs, err := reg.Gather()
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
if want, got := expectedNumMetrics, len(mfs); want != got {
130+
t.Fatalf("unexpected number of metric families gathered, want %d, got %d", want, got)
131+
}
132+
133+
for _, mf := range mfs {
134+
if len(mf.Metric) == 0 {
135+
t.Errorf("metric family %s must not be empty", mf.GetName())
136+
}
137+
for _, m := range mf.GetMetric() {
138+
if c := m.GetCounter(); c != nil {
139+
if len(expectedExemplar) == 0 {
140+
if c.Exemplar != nil {
141+
t.Errorf("expected no exemplar on the counter %v%v, got %v", mf.GetName(), m.Label, c.Exemplar.String())
142+
}
143+
continue
144+
}
145+
146+
if c.Exemplar == nil {
147+
t.Errorf("expected exemplar %v on the counter %v%v, got none", expectedExemplar, mf.GetName(), m.Label)
148+
continue
149+
}
150+
if got := c.Exemplar.Label; !reflect.DeepEqual(expectedExemplar, got) {
151+
t.Errorf("expected exemplar %v on the counter %v%v, got %v", expectedExemplar, mf.GetName(), m.Label, got)
152+
}
153+
continue
154+
}
155+
if h := m.GetHistogram(); h != nil {
156+
found := false
157+
for _, b := range h.GetBucket() {
158+
if len(expectedExemplar) == 0 {
159+
if b.Exemplar != nil {
160+
t.Errorf("expected no exemplar on histogram %v%v bkt %v, got %v", mf.GetName(), m.Label, b.GetUpperBound(), b.Exemplar.String())
161+
}
162+
continue
163+
}
164+
165+
if b.Exemplar == nil {
166+
continue
167+
}
168+
if got := b.Exemplar.Label; !reflect.DeepEqual(expectedExemplar, got) {
169+
t.Errorf("expected exemplar %v on the histogram %v%v on bkt %v, got %v", expectedExemplar, mf.GetName(), m.Label, b.GetUpperBound(), got)
170+
continue
171+
}
172+
found = true
173+
break
174+
}
175+
176+
if len(expectedExemplar) > 0 && !found {
177+
t.Errorf("expected exemplar %v on at least one bucket of the histogram %v%v, got none", expectedExemplar, mf.GetName(), m.Label)
178+
}
179+
}
180+
}
181+
}
182+
}
183+
101184
func TestClientMiddlewareAPI(t *testing.T) {
102185
client, reg := makeInstrumentedClient()
103186
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -111,21 +194,28 @@ func TestClientMiddlewareAPI(t *testing.T) {
111194
}
112195
defer resp.Body.Close()
113196

114-
mfs, err := reg.Gather()
197+
assetMetricAndExemplars(t, reg, 3, nil)
198+
}
199+
200+
func TestClientMiddlewareAPI_WithExemplars(t *testing.T) {
201+
exemplar := prometheus.Labels{"traceID": "example situation observed by this metric"}
202+
203+
client, reg := makeInstrumentedClient(WithExemplarFromContext(func(_ context.Context) prometheus.Labels { return exemplar }))
204+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
205+
w.WriteHeader(http.StatusOK)
206+
}))
207+
defer backend.Close()
208+
209+
resp, err := client.Get(backend.URL)
115210
if err != nil {
116211
t.Fatal(err)
117212
}
118-
if want, got := 3, len(mfs); want != got {
119-
t.Fatalf("unexpected number of metric families gathered, want %d, got %d", want, got)
120-
}
121-
for _, mf := range mfs {
122-
if len(mf.Metric) == 0 {
123-
t.Errorf("metric family %s must not be empty", mf.GetName())
124-
}
125-
}
213+
defer resp.Body.Close()
214+
215+
assetMetricAndExemplars(t, reg, 3, labelsToLabelPair(exemplar))
126216
}
127217

128-
func TestClientMiddlewareAPIWithRequestContext(t *testing.T) {
218+
func TestClientMiddlewareAPI_WithRequestContext(t *testing.T) {
129219
client, reg := makeInstrumentedClient()
130220
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131221
w.WriteHeader(http.StatusOK)

0 commit comments

Comments
 (0)