github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/middleware/instrument.go (about) 1 package middleware 2 3 import ( 4 "io" 5 "net/http" 6 "regexp" 7 "strconv" 8 "strings" 9 10 "github.com/felixge/httpsnoop" 11 "github.com/gorilla/mux" 12 "github.com/prometheus/client_golang/prometheus" 13 14 "github.com/weaveworks/common/instrument" 15 ) 16 17 const mb = 1024 * 1024 18 19 // BodySizeBuckets defines buckets for request/response body sizes. 20 var BodySizeBuckets = []float64{1 * mb, 2.5 * mb, 5 * mb, 10 * mb, 25 * mb, 50 * mb, 100 * mb, 250 * mb} 21 22 // RouteMatcher matches routes 23 type RouteMatcher interface { 24 Match(*http.Request, *mux.RouteMatch) bool 25 } 26 27 // Instrument is a Middleware which records timings for every HTTP request 28 type Instrument struct { 29 RouteMatcher RouteMatcher 30 Duration *prometheus.HistogramVec 31 RequestBodySize *prometheus.HistogramVec 32 ResponseBodySize *prometheus.HistogramVec 33 InflightRequests *prometheus.GaugeVec 34 } 35 36 // IsWSHandshakeRequest returns true if the given request is a websocket handshake request. 37 func IsWSHandshakeRequest(req *http.Request) bool { 38 if strings.ToLower(req.Header.Get("Upgrade")) == "websocket" { 39 // Connection header values can be of form "foo, bar, ..." 40 parts := strings.Split(strings.ToLower(req.Header.Get("Connection")), ",") 41 for _, part := range parts { 42 if strings.TrimSpace(part) == "upgrade" { 43 return true 44 } 45 } 46 } 47 return false 48 } 49 50 // Wrap implements middleware.Interface 51 func (i Instrument) Wrap(next http.Handler) http.Handler { 52 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 route := i.getRouteName(r) 54 inflight := i.InflightRequests.WithLabelValues(r.Method, route) 55 inflight.Inc() 56 defer inflight.Dec() 57 58 origBody := r.Body 59 defer func() { 60 // No need to leak our Body wrapper beyond the scope of this handler. 61 r.Body = origBody 62 }() 63 64 rBody := &reqBody{b: origBody} 65 r.Body = rBody 66 67 isWS := strconv.FormatBool(IsWSHandshakeRequest(r)) 68 69 respMetrics := httpsnoop.CaptureMetricsFn(w, func(ww http.ResponseWriter) { 70 next.ServeHTTP(ww, r) 71 }) 72 73 i.RequestBodySize.WithLabelValues(r.Method, route).Observe(float64(rBody.read)) 74 i.ResponseBodySize.WithLabelValues(r.Method, route).Observe(float64(respMetrics.Written)) 75 76 instrument.ObserveWithExemplar(r.Context(), i.Duration.WithLabelValues(r.Method, route, strconv.Itoa(respMetrics.Code), isWS), respMetrics.Duration.Seconds()) 77 }) 78 } 79 80 // Return a name identifier for ths request. There are three options: 81 // 1. The request matches a gorilla mux route, with a name. Use that. 82 // 2. The request matches an unamed gorilla mux router. Munge the path 83 // template such that templates like '/api/{org}/foo' come out as 84 // 'api_org_foo'. 85 // 3. The request doesn't match a mux route. Return "other" 86 // We do all this as we do not wish to emit high cardinality labels to 87 // prometheus. 88 func (i Instrument) getRouteName(r *http.Request) string { 89 route := getRouteName(i.RouteMatcher, r) 90 if route == "" { 91 route = "other" 92 } 93 94 return route 95 } 96 97 func getRouteName(routeMatcher RouteMatcher, r *http.Request) string { 98 var routeMatch mux.RouteMatch 99 if routeMatcher == nil || !routeMatcher.Match(r, &routeMatch) { 100 return "" 101 } 102 103 if routeMatch.MatchErr == mux.ErrNotFound { 104 return "notfound" 105 } 106 107 if routeMatch.Route == nil { 108 return "" 109 } 110 111 if name := routeMatch.Route.GetName(); name != "" { 112 return name 113 } 114 115 tmpl, err := routeMatch.Route.GetPathTemplate() 116 if err == nil { 117 return MakeLabelValue(tmpl) 118 } 119 120 return "" 121 } 122 123 var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) 124 125 // MakeLabelValue converts a Gorilla mux path to a string suitable for use in 126 // a Prometheus label value. 127 func MakeLabelValue(path string) string { 128 // Convert non-alnums to underscores. 129 result := invalidChars.ReplaceAllString(path, "_") 130 131 // Trim leading and trailing underscores. 132 result = strings.Trim(result, "_") 133 134 // Make it all lowercase 135 result = strings.ToLower(result) 136 137 // Special case. 138 if result == "" { 139 result = "root" 140 } 141 return result 142 } 143 144 type reqBody struct { 145 b io.ReadCloser 146 read int64 147 } 148 149 func (w *reqBody) Read(p []byte) (int, error) { 150 n, err := w.b.Read(p) 151 if n > 0 { 152 w.read += int64(n) 153 } 154 return n, err 155 } 156 157 func (w *reqBody) Close() error { 158 return w.b.Close() 159 }