github.com/openshift-online/ocm-sdk-go@v0.1.473/metrics/handler_wrapper.go (about)

     1  /*
     2  Copyright (c) 2021 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // This file contains the implementations of a handler wrapper that generates Prometheus metrics.
    18  
    19  package metrics
    20  
    21  import (
    22  	"fmt"
    23  	"net/http"
    24  	"time"
    25  
    26  	"github.com/prometheus/client_golang/prometheus"
    27  )
    28  
    29  // HandlerWrapperBuilder contains the data and logic needed to build a new metrics handler wrapper
    30  // that creates HTTP handlers that generate the following Prometheus metrics:
    31  //
    32  //	<subsystem>_request_count - Number of API requests sent.
    33  //	<subsystem>_request_duration_sum - Total time to send API requests, in seconds.
    34  //	<subsystem>_request_duration_count - Total number of API requests measured.
    35  //	<subsystem>_request_duration_bucket - Number of API requests organized in buckets.
    36  //
    37  // To set the subsystem prefix use the Subsystem method.
    38  //
    39  // The duration buckets metrics contain an `le` label that indicates the upper bound. For example if
    40  // the `le` label is `1` then the value will be the number of requests that were processed in less
    41  // than one second.
    42  //
    43  // The metrics will have the following labels:
    44  //
    45  //	method - Name of the HTTP method, for example GET or POST.
    46  //	path - Request path, for example /api/clusters_mgmt/v1/clusters.
    47  //	code - HTTP response code, for example 200 or 500.
    48  //	apiservice - API service name, for example ocm-clusters-service.
    49  //
    50  // To calculate the average request duration during the last 10 minutes, for example, use a
    51  // Prometheus expression like this:
    52  //
    53  //	rate(api_outbound_request_duration_sum[10m]) / rate(api_outbound_request_duration_count[10m])
    54  //
    55  // In order to reduce the cardinality of the metrics the path label is modified to remove the
    56  // identifiers of the objects. For example, if the original path is .../clusters/123 then it will
    57  // be replaced by .../clusters/-, and the values will be accumulated. The line returned by the
    58  // metrics server will be like this:
    59  //
    60  //	     <subsystem>_request_count{code="200",method="GET",path="/api/clusters_mgmt/v1/clusters/-",
    61  //			apiservice="ocm-clusters-service"} 56
    62  //
    63  // The meaning of that is that there were a total of 56 requests to get specific clusters,
    64  // independently of the specific identifier of the cluster.
    65  //
    66  // The value of the `code` label will be zero when sending the request failed without a response
    67  // code, for example if it wasn't possible to open the connection, or if there was a timeout waiting
    68  // for the response.
    69  //
    70  // Note that setting this attribute is not enough to have metrics published, you also need to
    71  // create and start a metrics server, as described in the documentation of the Prometheus library.
    72  //
    73  // Don't create objects of this type directly; use the NewHandlerWrapper function instead.
    74  type HandlerWrapperBuilder struct {
    75  	paths      []string
    76  	subsystem  string
    77  	registerer prometheus.Registerer
    78  }
    79  
    80  // HandlerWrapper contains the data and logic needed to wrap an HTTP handler with another one that
    81  // generates Prometheus metrics.
    82  type HandlerWrapper struct {
    83  	paths           pathTree
    84  	requestCount    *prometheus.CounterVec
    85  	requestDuration *prometheus.HistogramVec
    86  }
    87  
    88  // handler is an HTTP handler that generates Prometheus metrics.
    89  type handler struct {
    90  	owner   *HandlerWrapper
    91  	handler http.Handler
    92  }
    93  
    94  // Make sure that we implement the interface:
    95  var _ http.Handler = (*handler)(nil)
    96  
    97  // responseWriter is the HTTP response writer used to obtain the response code.
    98  type responseWriter struct {
    99  	code   int
   100  	writer http.ResponseWriter
   101  }
   102  
   103  // Make sure that we implement the interface:
   104  var _ http.ResponseWriter = (*responseWriter)(nil)
   105  
   106  // NewHandlerWrapper creates a new builder that can then be used to configure and create a new
   107  // metrics handler wrapper.
   108  func NewHandlerWrapper() *HandlerWrapperBuilder {
   109  	return &HandlerWrapperBuilder{
   110  		registerer: prometheus.DefaultRegisterer,
   111  	}
   112  }
   113  
   114  // Path adds a path that will be accepted as a value for the `path` label. By default all the paths
   115  // of the API are already added. This is intended for additional pads, for example the path for
   116  // token requests. If those paths aren't explicitly specified here then their metrics will be
   117  // accumulated in the `/-` path.
   118  func (b *HandlerWrapperBuilder) Path(value string) *HandlerWrapperBuilder {
   119  	b.paths = append(b.paths, value)
   120  	return b
   121  }
   122  
   123  // Subsystem sets the name of the subsystem that will be used by to register the metrics with
   124  // Prometheus. For example, if the value is `api_inbound` then the following metrics will be
   125  // registered:
   126  //
   127  //	api_inbound_request_count - Number of API requests sent.
   128  //	api_inbound_request_duration_sum - Total time to send API requests, in seconds.
   129  //	api_inbound_request_duration_count - Total number of API requests measured.
   130  //	api_inbound_request_duration_bucket - Number of API requests organized in buckets.
   131  //
   132  // This is mandatory.
   133  func (b *HandlerWrapperBuilder) Subsystem(value string) *HandlerWrapperBuilder {
   134  	b.subsystem = value
   135  	return b
   136  }
   137  
   138  // Registerer sets the Prometheus registerer that will be used to register the metrics. The default
   139  // is to use the default Prometheus registerer and there is usually no need to change that. This is
   140  // intended for unit tests, where it is convenient to have a registerer that doesn't interfere with
   141  // the rest of the system.
   142  func (b *HandlerWrapperBuilder) Registerer(value prometheus.Registerer) *HandlerWrapperBuilder {
   143  	if value == nil {
   144  		value = prometheus.DefaultRegisterer
   145  	}
   146  	b.registerer = value
   147  	return b
   148  }
   149  
   150  // Build uses the information stored in the builder to create a new handler wrapper.
   151  func (b *HandlerWrapperBuilder) Build() (result *HandlerWrapper, err error) {
   152  	// Check parameters:
   153  	if b.subsystem == "" {
   154  		err = fmt.Errorf("subsystem is mandatory")
   155  		return
   156  	}
   157  
   158  	// Register the request count metric:
   159  	requestCount := prometheus.NewCounterVec(
   160  		prometheus.CounterOpts{
   161  			Subsystem: b.subsystem,
   162  			Name:      "request_count",
   163  			Help:      "Number of requests sent.",
   164  		},
   165  		requestLabelNames,
   166  	)
   167  	err = b.registerer.Register(requestCount)
   168  	if err != nil {
   169  		registered, ok := err.(prometheus.AlreadyRegisteredError)
   170  		if ok {
   171  			requestCount = registered.ExistingCollector.(*prometheus.CounterVec)
   172  			err = nil //nolint:all
   173  		} else {
   174  			return
   175  		}
   176  	}
   177  
   178  	// Create the path tree:
   179  	paths := pathRoot.copy()
   180  	for _, path := range b.paths {
   181  		paths.add(path)
   182  	}
   183  
   184  	// Register the request duration metric:
   185  	requestDuration := prometheus.NewHistogramVec(
   186  		prometheus.HistogramOpts{
   187  			Subsystem: b.subsystem,
   188  			Name:      "request_duration",
   189  			Help:      "Request duration in seconds.",
   190  			Buckets: []float64{
   191  				0.1,
   192  				1.0,
   193  				10.0,
   194  				30.0,
   195  			},
   196  		},
   197  		requestLabelNames,
   198  	)
   199  	err = b.registerer.Register(requestDuration)
   200  	if err != nil {
   201  		registered, ok := err.(prometheus.AlreadyRegisteredError)
   202  		if ok {
   203  			requestDuration = registered.ExistingCollector.(*prometheus.HistogramVec)
   204  			err = nil
   205  		} else {
   206  			return
   207  		}
   208  	}
   209  
   210  	// Create and populate the object:
   211  	result = &HandlerWrapper{
   212  		paths:           paths,
   213  		requestCount:    requestCount,
   214  		requestDuration: requestDuration,
   215  	}
   216  
   217  	return
   218  }
   219  
   220  // Wrap creates a new handler that wraps the given one and generates the Prometheus metrics.
   221  func (w *HandlerWrapper) Wrap(h http.Handler) http.Handler {
   222  	return &handler{
   223  		owner:   w,
   224  		handler: h,
   225  	}
   226  }
   227  
   228  // ServeHTTP is the implementation of the HTTP handler interface.
   229  func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   230  	// We need to replace the response writer with a custom one that captures the response code
   231  	// generated by the next handler:
   232  	writer := responseWriter{
   233  		code:   http.StatusOK,
   234  		writer: w,
   235  	}
   236  
   237  	// Measure the time that it takes to process the request and send the response:
   238  	start := time.Now()
   239  	h.handler.ServeHTTP(&writer, r)
   240  	elapsed := time.Since(start)
   241  
   242  	// Update the metrics:
   243  	path := r.URL.Path
   244  	method := r.Method
   245  	labels := prometheus.Labels{
   246  		serviceLabelName: serviceLabel(path),
   247  		methodLabelName:  methodLabel(method),
   248  		pathLabelName:    pathLabel(h.owner.paths, path),
   249  		codeLabelName:    codeLabel(writer.code),
   250  	}
   251  	h.owner.requestCount.With(labels).Inc()
   252  	h.owner.requestDuration.With(labels).Observe(elapsed.Seconds())
   253  }
   254  
   255  // Header is part of the implementation of the http.ResponseWriter interface.
   256  func (w *responseWriter) Header() http.Header {
   257  	return w.writer.Header()
   258  }
   259  
   260  // Write is part of the implementation of the http.ResponseWriter interface.
   261  func (w *responseWriter) Write(b []byte) (n int, err error) {
   262  	n, err = w.writer.Write(b)
   263  	return
   264  }
   265  
   266  // WriteHeader is part of the implementation of the http.ResponseWriter interface.
   267  func (w *responseWriter) WriteHeader(code int) {
   268  	w.code = code
   269  	w.writer.WriteHeader(code)
   270  }
   271  
   272  // Flush is the implementation of the http.Flusher interface.
   273  func (w *responseWriter) Flush() {
   274  	flusher, ok := w.writer.(http.Flusher)
   275  	if ok {
   276  		flusher.Flush()
   277  	}
   278  }