github.com/letsencrypt/boulder@v0.20251208.0/metrics/measured_http/http.go (about) 1 package measured_http 2 3 import ( 4 "net/http" 5 "strconv" 6 7 "github.com/jmhodges/clock" 8 "github.com/prometheus/client_golang/prometheus" 9 "github.com/prometheus/client_golang/prometheus/promauto" 10 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 11 ) 12 13 // responseWriterWithStatus satisfies http.ResponseWriter, but keeps track of the 14 // status code for gathering stats. 15 type responseWriterWithStatus struct { 16 http.ResponseWriter 17 code int 18 } 19 20 // WriteHeader stores a status code for generating stats. 21 func (r *responseWriterWithStatus) WriteHeader(code int) { 22 r.code = code 23 r.ResponseWriter.WriteHeader(code) 24 } 25 26 // Write writes the body and sets the status code to 200 if a status code 27 // has not already been set. 28 func (r *responseWriterWithStatus) Write(body []byte) (int, error) { 29 if r.code == 0 { 30 r.code = http.StatusOK 31 } 32 return r.ResponseWriter.Write(body) 33 } 34 35 // serveMux is a partial interface wrapper for the one method http.ServeMux 36 // exposes that we use. This prevents us from accidentally developing an 37 // overly-specific reliance on that concrete type. 38 type serveMux interface { 39 Handler(*http.Request) (http.Handler, string) 40 } 41 42 // MeasuredHandler wraps an http.Handler and records prometheus stats 43 type MeasuredHandler struct { 44 serveMux 45 clk clock.Clock 46 // Normally this is always responseTime, but we override it for testing. 47 stat *prometheus.HistogramVec 48 // inFlightRequestsGauge is a gauge that tracks the number of requests 49 // currently in flight, labeled by endpoint. 50 inFlightRequestsGauge *prometheus.GaugeVec 51 } 52 53 func New(m serveMux, clk clock.Clock, stats prometheus.Registerer, opts ...otelhttp.Option) http.Handler { 54 responseTime := promauto.With(stats).NewHistogramVec(prometheus.HistogramOpts{ 55 Name: "response_time", 56 Help: "Time taken to respond to a request", 57 }, []string{"endpoint", "method", "code"}) 58 59 inFlightRequestsGauge := promauto.With(stats).NewGaugeVec(prometheus.GaugeOpts{ 60 Name: "in_flight_requests", 61 Help: "Tracks the number of WFE requests currently in flight, labeled by endpoint.", 62 }, []string{"endpoint"}) 63 64 return otelhttp.NewHandler(&MeasuredHandler{ 65 serveMux: m, 66 clk: clk, 67 stat: responseTime, 68 inFlightRequestsGauge: inFlightRequestsGauge, 69 }, "server", opts...) 70 } 71 72 func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 73 begin := h.clk.Now() 74 rwws := &responseWriterWithStatus{w, 0} 75 76 subHandler, pattern := h.Handler(r) 77 h.inFlightRequestsGauge.WithLabelValues(pattern).Inc() 78 defer h.inFlightRequestsGauge.WithLabelValues(pattern).Dec() 79 80 // Use the method string only if it's a recognized HTTP method. This avoids 81 // ballooning timeseries with invalid methods from public input. 82 var method string 83 switch r.Method { 84 case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, 85 http.MethodPatch, http.MethodDelete, http.MethodConnect, 86 http.MethodOptions, http.MethodTrace: 87 method = r.Method 88 default: 89 method = "unknown" 90 } 91 92 defer func() { 93 h.stat.With(prometheus.Labels{ 94 "endpoint": pattern, 95 "method": method, 96 "code": strconv.Itoa(rwws.code), 97 }).Observe(h.clk.Since(begin).Seconds()) 98 }() 99 100 subHandler.ServeHTTP(rwws, r) 101 }