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  }