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 }