Let a handler flush or set a write deadline through the access log and metrics (closes #191)
check / check (push) Waiting to run
check / check (push) Waiting to run
The access log's response writer and the metrics middleware's writer hid the writer beneath them, so a handler's flush, hijack or write deadline set through http.ResponseController failed with "not supported" behind them. The access log's writer now has Unwrap. The metrics middleware calls the library's public Measure with a writer of our own that has Unwrap, in place of std.Handler's writer, so the middleware order and what metrics record are unchanged. A test over a real connection sets a write deadline and flushes, metrics on and off, on a global route and in an admin page group, and fails without either Unwrap. Model: opus-5-5
This commit was merged in pull request #428.
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/go-chi/chi"
|
||||
httpmetrics "github.com/slok/go-http-metrics/metrics"
|
||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||
"github.com/slok/go-http-metrics/middleware/std"
|
||||
)
|
||||
|
||||
// inflightHandler is the fixed `handler` label on
|
||||
@@ -169,13 +168,72 @@ func metricsMiddleware(
|
||||
})
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
// The handler id is unmatchedRoute rather than "" so that
|
||||
// the client-chosen URL path never enters the metrics
|
||||
// pipeline at all: an empty id is the library's signal to
|
||||
// substitute it. boundedLabelRecorder overwrites this value
|
||||
// on every observation, so it is reachable only if that
|
||||
// decorator is removed — in which case the metrics collapse
|
||||
// to one series instead of leaking again.
|
||||
return std.Handler(unmatchedRoute, mdlw, next)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mw := &metricsResponseWriter{
|
||||
ResponseWriter: w,
|
||||
request: r,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// The handler id is unmatchedRoute rather than "" so
|
||||
// that the client-chosen URL path never enters the
|
||||
// metrics pipeline at all: an empty id is the library's
|
||||
// signal to substitute it. boundedLabelRecorder
|
||||
// overwrites this value on every observation, so it is
|
||||
// reachable only if that decorator is removed — in which
|
||||
// case the metrics collapse to one series instead of
|
||||
// leaking again.
|
||||
mdlw.Measure(unmatchedRoute, mw, func() {
|
||||
next.ServeHTTP(mw, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// metricsResponseWriter records the status code and body size of a
|
||||
// response, and hands them with the request to go-http-metrics'
|
||||
// Measure as its Reporter.
|
||||
//
|
||||
// It stands in for the library's std.Handler, whose writer has no
|
||||
// Unwrap: behind it, http.ResponseController cannot reach net/http's
|
||||
// own writer, so a handler's write deadline fails with metrics on.
|
||||
type metricsResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
request *http.Request
|
||||
statusCode int
|
||||
bytesWritten int64
|
||||
}
|
||||
|
||||
func (w *metricsResponseWriter) WriteHeader(code int) {
|
||||
w.statusCode = code
|
||||
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *metricsResponseWriter) Write(b []byte) (int, error) {
|
||||
w.bytesWritten += int64(len(b))
|
||||
|
||||
//nolint:wrapcheck // Pass the writer's own error through unchanged.
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// Unwrap lets http.ResponseController reach the writer underneath, so
|
||||
// a handler can still flush or set a write deadline with metrics on.
|
||||
func (w *metricsResponseWriter) Unwrap() http.ResponseWriter {
|
||||
return w.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *metricsResponseWriter) Method() string { return w.request.Method }
|
||||
|
||||
func (w *metricsResponseWriter) Context() context.Context {
|
||||
return w.request.Context()
|
||||
}
|
||||
|
||||
func (w *metricsResponseWriter) URLPath() string { return w.request.URL.Path }
|
||||
|
||||
func (w *metricsResponseWriter) StatusCode() int { return w.statusCode }
|
||||
|
||||
func (w *metricsResponseWriter) BytesWritten() int64 { return w.bytesWritten }
|
||||
|
||||
var _ ghmm.Reporter = (*metricsResponseWriter)(nil)
|
||||
|
||||
Reference in New Issue
Block a user