Skip to content

Commit 2b3ab50

Browse files
stuartnelson3beorn7
authored andcommitted
Add time to write header handler middleware (#304)
1 parent 42552c1 commit 2b3ab50

File tree

4 files changed

+81
-14
lines changed

4 files changed

+81
-14
lines changed

prometheus/promhttp/delegator_1_7.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ import (
2020
"net/http"
2121
)
2222

23-
func newDelegator(w http.ResponseWriter) delegator {
24-
d := &responseWriterDelegator{ResponseWriter: w}
23+
func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator {
24+
d := &responseWriterDelegator{
25+
ResponseWriter: w,
26+
observeWriteHeader: observeWriteHeaderFunc,
27+
}
2528

2629
_, cn := w.(http.CloseNotifier)
2730
_, fl := w.(http.Flusher)

prometheus/promhttp/delegator_1_8.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import (
2222

2323
// newDelegator handles the four different methods of upgrading a
2424
// http.ResponseWriter to delegator.
25-
func newDelegator(w http.ResponseWriter) delegator {
26-
d := &responseWriterDelegator{ResponseWriter: w}
25+
func newDelegator(w http.ResponseWriter, observeWriteHeaderFunc func(int)) delegator {
26+
d := &responseWriterDelegator{
27+
ResponseWriter: w,
28+
observeWriteHeader: observeWriteHeaderFunc,
29+
}
2730

2831
_, cn := w.(http.CloseNotifier)
2932
_, fl := w.(http.Flusher)

prometheus/promhttp/instrument_server.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler) ht
6363
if code {
6464
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6565
now := time.Now()
66-
d := newDelegator(w)
66+
d := newDelegator(w, nil)
6767
next.ServeHTTP(d, r)
6868

6969
obs.With(labels(code, method, r.Method, d.Status())).Observe(time.Since(now).Seconds())
@@ -96,7 +96,7 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler)
9696

9797
if code {
9898
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99-
d := newDelegator(w)
99+
d := newDelegator(w, nil)
100100
next.ServeHTTP(d, r)
101101
counter.With(labels(code, method, r.Method, d.Status())).Inc()
102102
})
@@ -108,6 +108,34 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler)
108108
})
109109
}
110110

111+
// InstrumentHandlerTimeToWriteHeader is a middleware that wraps the provided
112+
// http.Handler to observe with the provided ObserverVec the request duration
113+
// until the response headers are written. The ObserverVec must have zero, one,
114+
// or two labels. The only allowed label names are "code" and "method". The
115+
// function panics if any other instance labels are provided. The Observe
116+
// method of the Observer in the ObserverVec is called with the request
117+
// duration in seconds. Partitioning happens by HTTP status code and/or HTTP
118+
// method if the respective instance label names are present in the
119+
// ObserverVec. For unpartitioned observations, use an ObserverVec with zero
120+
// labels. Note that partitioning of Histograms is expensive and should be used
121+
// judiciously.
122+
//
123+
// If the wrapped Handler panics before calling WriteHeader, no value is
124+
// reported.
125+
//
126+
// See the example for InstrumentHandlerDuration for example usage.
127+
func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc {
128+
code, method := checkLabels(obs)
129+
130+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131+
now := time.Now()
132+
d := newDelegator(w, func(status int) {
133+
obs.With(labels(code, method, r.Method, status)).Observe(time.Since(now).Seconds())
134+
})
135+
next.ServeHTTP(d, r)
136+
})
137+
}
138+
111139
// InstrumentHandlerRequestSize is a middleware that wraps the provided
112140
// http.Handler to observe the request size with the provided ObserverVec.
113141
// The ObserverVec must have zero, one, or two labels. The only allowed label
@@ -129,7 +157,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler)
129157

130158
if code {
131159
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132-
d := newDelegator(w)
160+
d := newDelegator(w, nil)
133161
next.ServeHTTP(d, r)
134162
size := computeApproximateRequestSize(r)
135163
obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(size))
@@ -162,7 +190,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler)
162190
func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler) http.Handler {
163191
code, method := checkLabels(obs)
164192
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165-
d := newDelegator(w)
193+
d := newDelegator(w, nil)
166194
next.ServeHTTP(d, r)
167195
obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(d.Written()))
168196
})
@@ -418,10 +446,11 @@ type delegator interface {
418446
type responseWriterDelegator struct {
419447
http.ResponseWriter
420448

421-
handler, method string
422-
status int
423-
written int64
424-
wroteHeader bool
449+
handler, method string
450+
status int
451+
written int64
452+
wroteHeader bool
453+
observeWriteHeader func(int)
425454
}
426455

427456
func (r *responseWriterDelegator) Status() int {
@@ -436,6 +465,9 @@ func (r *responseWriterDelegator) WriteHeader(code int) {
436465
r.status = code
437466
r.wroteHeader = true
438467
r.ResponseWriter.WriteHeader(code)
468+
if r.observeWriteHeader != nil {
469+
r.observeWriteHeader(code)
470+
}
439471
}
440472

441473
func (r *responseWriterDelegator) Write(b []byte) (int, error) {

prometheus/promhttp/instrument_server_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ func TestMiddlewareAPI(t *testing.T) {
4848
[]string{"method"},
4949
)
5050

51+
writeHeaderVec := prometheus.NewHistogramVec(
52+
prometheus.HistogramOpts{
53+
Name: "write_header_duration_seconds",
54+
Help: "A histogram of time to first write latencies.",
55+
Buckets: prometheus.DefBuckets,
56+
ConstLabels: prometheus.Labels{"handler": "api"},
57+
},
58+
[]string{},
59+
)
60+
5161
responseSize := prometheus.NewHistogramVec(
5262
prometheus.HistogramOpts{
5363
Name: "push_request_size_bytes",
@@ -61,12 +71,14 @@ func TestMiddlewareAPI(t *testing.T) {
6171
w.Write([]byte("OK"))
6272
})
6373

64-
reg.MustRegister(inFlightGauge, counter, histVec, responseSize)
74+
reg.MustRegister(inFlightGauge, counter, histVec, responseSize, writeHeaderVec)
6575

6676
chain := InstrumentHandlerInFlight(inFlightGauge,
6777
InstrumentHandlerCounter(counter,
6878
InstrumentHandlerDuration(histVec,
69-
InstrumentHandlerResponseSize(responseSize, handler),
79+
InstrumentHandlerTimeToWriteHeader(writeHeaderVec,
80+
InstrumentHandlerResponseSize(responseSize, handler),
81+
),
7082
),
7183
),
7284
)
@@ -76,6 +88,23 @@ func TestMiddlewareAPI(t *testing.T) {
7688
chain.ServeHTTP(w, r)
7789
}
7890

91+
func TestInstrumentTimeToFirstWrite(t *testing.T) {
92+
var i int
93+
dobs := &responseWriterDelegator{
94+
ResponseWriter: httptest.NewRecorder(),
95+
observeWriteHeader: func(status int) {
96+
i = status
97+
},
98+
}
99+
d := newDelegator(dobs, nil)
100+
101+
d.WriteHeader(http.StatusOK)
102+
103+
if i != http.StatusOK {
104+
t.Fatalf("failed to execute observeWriteHeader")
105+
}
106+
}
107+
79108
func ExampleInstrumentHandlerDuration() {
80109
inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{
81110
Name: "in_flight_requests",

0 commit comments

Comments
 (0)