github.com/alwitt/goutils@v0.6.4/metrics.go (about) 1 package goutils 2 3 import ( 4 "context" 5 "net/http" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/apex/log" 11 "github.com/gorilla/mux" 12 "github.com/prometheus/client_golang/prometheus" 13 "github.com/prometheus/client_golang/prometheus/collectors" 14 "github.com/prometheus/client_golang/prometheus/promhttp" 15 ) 16 17 // Standard metrics provided with the package 18 const ( 19 // ==================================================================================== 20 // HTTP 21 22 // metricsNameHTTPRequest HTTP request tracking. Additional parameters are attached via labels 23 // 24 // - method: [GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS] 25 // 26 // - status code ENUM: [2XX, 3XX, 4XX, 5XX+] 27 metricsNameHTTPRequest = "http_request_total" 28 29 // metricsNameHTTPRequestLatency HTTP request latency tracking. Additional parameters are 30 // attached via labels 31 // 32 // - method: [GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS] 33 // 34 // - status code ENUM: [2XX, 3XX, 4XX, 5XX+] 35 metricsNameHTTPRequestLatency = "http_request_latency_secs_total" 36 37 // metricsNameHTTPResponseSize HTTP response size tracking. Additional parameters are 38 // attached via labels 39 // 40 // - method: [GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS] 41 // 42 // - status code ENUM: [2XX, 3XX, 4XX, 5XX+] 43 metricsNameHTTPResponseSize = "http_response_size_bytes_total" 44 45 // ==================================================================================== 46 // PubSub 47 48 // metricsNamePubSubPublish PubSub publish tracking. Additional parameters are attached 49 // via labels 50 // 51 // - topic 52 // 53 // - success 54 metricsNamePubSubPublish = "pubsub_publish_total" 55 56 // metricsNamePubSubPublishPayloadSize PubSub publish message size tracking. Additional 57 // parameters are attached via labels 58 // 59 // - topic 60 // 61 // - success 62 metricsNamePubSubPublishPayloadSize = "pubsub_publish_payload_size_bytes_total" 63 64 // metricsNamePubSubReceive PubSub receive message tracking. Additional parameters are 65 // attacked via labels 66 // 67 // - topic 68 // 69 // - success 70 metricsNamePubSubReceive = "pubsub_receive_total" 71 72 // metricsNamePubSubReceivePayloadSize PubSub receive message size tracking. Additional 73 // parameters are attached via labels 74 // 75 // - topic 76 // 77 // - success 78 metricsNamePubSubReceivePayloadSize = "pubsub_receive_payload_size_bytes_total" 79 ) 80 81 // Standard metrics labels provided with the package 82 const ( 83 // ==================================================================================== 84 // HTTP 85 86 // labelNameHTTPMethod HTTP request method label name 87 labelNameHTTPMethod = "method" 88 89 // labelNameHTTPStatus HTTP status label name 90 labelNameHTTPStatus = "status" 91 92 // ==================================================================================== 93 // PubSub 94 95 // labelNamePubSubTopic PubSub topic label name 96 labelNamePubSubTopic = "topic" 97 98 // labelNamePubSubSuccess whether processing is successful or not 99 labelNamePubSubSuccess = "success" 100 ) 101 102 // MetricsCollector metrics collection support client 103 type MetricsCollector interface { 104 /* 105 InstallApplicationMetrics install trackers for Golang application execution metrics 106 */ 107 InstallApplicationMetrics() 108 109 /* 110 InstallHTTPMetrics install trackers for HTTP request metrics collection. This will return 111 a helper agent to record the metrics. 112 113 @returns request metrics logging agent 114 */ 115 InstallHTTPMetrics() HTTPRequestMetricHelper 116 117 /* 118 InstallPubSubMetrics install trackers for PubSub messaging collection. This will return 119 a helper agent to record the metrics. 120 121 @return PubSub metrics logging agent 122 */ 123 InstallPubSubMetrics() PubSubMetricHelper 124 125 /* 126 InstallCustomCounterVecMetrics install new custom `CounterVec` metrics 127 128 @param ctxt context.Context - execution context 129 @param metricsName string - metrics name 130 @param metricsHelpMessage string - metrics help message 131 @param metricsLabels []string - labels to support 132 @returns new `CounterVec` handle 133 */ 134 InstallCustomCounterVecMetrics( 135 ctxt context.Context, metricsName string, metricsHelpMessage string, metricsLabels []string, 136 ) (*prometheus.CounterVec, error) 137 138 /* 139 InstallCustomGaugeVecMetrics install new custom `GaugeVec` metrics 140 141 @param ctxt context.Context - execution context 142 @param metricsName string - metrics name 143 @param metricsHelpMessage string - metrics help message 144 @param metricsLabels []string - labels to support 145 @returns new `GaugeVec` handle 146 */ 147 InstallCustomGaugeVecMetrics( 148 ctxt context.Context, metricsName string, metricsHelpMessage string, metricsLabels []string, 149 ) (*prometheus.GaugeVec, error) 150 151 /* 152 ExposeCollectionEndpoint expose the Prometheus metric collection endpoint 153 154 @param outer *mux.Router - HTTP router to install endpoint on 155 @param metricsPath string - metrics endpoint path relative to the router provided 156 @param maxSupportedRequest int - max number of request the endpoint will support 157 */ 158 ExposeCollectionEndpoint(router *mux.Router, metricsPath string, maxSupportedRequest int) 159 } 160 161 // metricsCollectorImpl implements MetricsCollector 162 type metricsCollectorImpl struct { 163 Component 164 lock sync.Mutex 165 prometheus *prometheus.Registry 166 httpMetrics HTTPRequestMetricHelper 167 pubsubMetrics PubSubMetricHelper 168 } 169 170 /* 171 GetNewMetricsCollector get metrics collection support client 172 173 @param logTags log.Fields - metadata fields to include in the logs 174 @param customLogModifiers []LogMetadataModifier - additional log metadata modifiers to use 175 @returns metric collection support client 176 */ 177 func GetNewMetricsCollector( 178 logTags log.Fields, 179 customLogModifiers []LogMetadataModifier, 180 ) (MetricsCollector, error) { 181 instance := &metricsCollectorImpl{ 182 Component: Component{ 183 LogTags: logTags, 184 LogTagModifiers: customLogModifiers, 185 }, 186 lock: sync.Mutex{}, 187 prometheus: prometheus.NewRegistry(), 188 httpMetrics: nil, 189 } 190 191 return instance, nil 192 } 193 194 func (c *metricsCollectorImpl) InstallApplicationMetrics() { 195 c.lock.Lock() 196 defer c.lock.Unlock() 197 c.prometheus.MustRegister( 198 collectors.NewGoCollector(), 199 collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), 200 ) 201 } 202 203 func (c *metricsCollectorImpl) InstallHTTPMetrics() HTTPRequestMetricHelper { 204 c.lock.Lock() 205 defer c.lock.Unlock() 206 207 if c.httpMetrics != nil { 208 return c.httpMetrics 209 } 210 211 requestTracker := prometheus.NewCounterVec( 212 prometheus.CounterOpts{ 213 Name: metricsNameHTTPRequest, 214 Help: "HTTP request tracking", 215 }, 216 []string{labelNameHTTPMethod, labelNameHTTPStatus}, 217 ) 218 latencyTracker := prometheus.NewCounterVec( 219 prometheus.CounterOpts{ 220 Name: metricsNameHTTPRequestLatency, 221 Help: "HTTP request latency tracking", 222 }, 223 []string{labelNameHTTPMethod, labelNameHTTPStatus}, 224 ) 225 respSizeTracker := prometheus.NewCounterVec( 226 prometheus.CounterOpts{ 227 Name: metricsNameHTTPResponseSize, 228 Help: "HTTP response size tracking", 229 }, 230 []string{labelNameHTTPMethod, labelNameHTTPStatus}, 231 ) 232 c.prometheus.MustRegister( 233 requestTracker, latencyTracker, respSizeTracker, 234 ) 235 c.httpMetrics = &httpRequestMetricHelperImpl{ 236 requestTracker: requestTracker, 237 latencyTracker: latencyTracker, 238 respSizeTracker: respSizeTracker, 239 } 240 return c.httpMetrics 241 } 242 243 func (c *metricsCollectorImpl) InstallPubSubMetrics() PubSubMetricHelper { 244 c.lock.Lock() 245 defer c.lock.Unlock() 246 247 if c.pubsubMetrics != nil { 248 return c.pubsubMetrics 249 } 250 251 publishTracker := prometheus.NewCounterVec( 252 prometheus.CounterOpts{ 253 Name: metricsNamePubSubPublish, 254 Help: "PubSub publish tracking", 255 }, 256 []string{labelNamePubSubTopic, labelNamePubSubSuccess}, 257 ) 258 publishPayloadTracker := prometheus.NewCounterVec( 259 prometheus.CounterOpts{ 260 Name: metricsNamePubSubPublishPayloadSize, 261 Help: "PubSub publish message size tracking", 262 }, 263 []string{labelNamePubSubTopic, labelNamePubSubSuccess}, 264 ) 265 receiveTracker := prometheus.NewCounterVec( 266 prometheus.CounterOpts{ 267 Name: metricsNamePubSubReceive, 268 Help: "PubSub receive message tracking", 269 }, 270 []string{labelNamePubSubTopic, labelNamePubSubSuccess}, 271 ) 272 receivePayloadTracker := prometheus.NewCounterVec( 273 prometheus.CounterOpts{ 274 Name: metricsNamePubSubReceivePayloadSize, 275 Help: "PubSub receive message size tracking", 276 }, 277 []string{labelNamePubSubTopic, labelNamePubSubSuccess}, 278 ) 279 c.prometheus.MustRegister( 280 publishTracker, publishPayloadTracker, receiveTracker, receivePayloadTracker, 281 ) 282 c.pubsubMetrics = &pubsubMetricHelperImpl{ 283 publishTracker: publishTracker, 284 publishPayloadTracker: publishPayloadTracker, 285 receiveTracker: receiveTracker, 286 receivePayloadTracker: receivePayloadTracker, 287 } 288 return c.pubsubMetrics 289 } 290 291 func (c *metricsCollectorImpl) InstallCustomCounterVecMetrics( 292 ctxt context.Context, metricsName string, metricsHelpMessage string, metricsLabels []string, 293 ) (*prometheus.CounterVec, error) { 294 logTags := c.GetLogTagsForContext(ctxt) 295 newMetricsTracker := prometheus.NewCounterVec( 296 prometheus.CounterOpts{Name: metricsName, Help: metricsHelpMessage}, metricsLabels, 297 ) 298 if err := c.prometheus.Register(newMetricsTracker); err != nil { 299 log. 300 WithError(err). 301 WithFields(logTags). 302 Errorf("Failed to register new metrics '%s'", metricsName) 303 return nil, err 304 } 305 return newMetricsTracker, nil 306 } 307 308 func (c *metricsCollectorImpl) InstallCustomGaugeVecMetrics( 309 ctxt context.Context, metricsName string, metricsHelpMessage string, metricsLabels []string, 310 ) (*prometheus.GaugeVec, error) { 311 logTags := c.GetLogTagsForContext(ctxt) 312 newMetricsTracker := prometheus.NewGaugeVec( 313 prometheus.GaugeOpts{Name: metricsName, Help: metricsHelpMessage}, metricsLabels, 314 ) 315 if err := c.prometheus.Register(newMetricsTracker); err != nil { 316 log. 317 WithError(err). 318 WithFields(logTags). 319 Errorf("Failed to register new metrics '%s'", metricsName) 320 return nil, err 321 } 322 return newMetricsTracker, nil 323 } 324 325 func (c *metricsCollectorImpl) ExposeCollectionEndpoint( 326 router *mux.Router, metricsPath string, maxSupportedRequest int, 327 ) { 328 router. 329 Path(metricsPath). 330 Methods("GET", "POST"). 331 Handler(promhttp.HandlerFor(c.prometheus, promhttp.HandlerOpts{ 332 MaxRequestsInFlight: maxSupportedRequest, 333 EnableOpenMetrics: true, 334 })) 335 } 336 337 // HTTPRequestMetricHelper HTTP request metric recording helper agent 338 type HTTPRequestMetricHelper interface { 339 /* 340 RecordRequest record parameters regarding a request to the metrics 341 342 @param method string - HTTP request method 343 @param status int - HTTP response status 344 @param latency time.Duration - delay between request received, and response sent 345 @param respSize int64 - HTTP response size in bytes 346 */ 347 RecordRequest(method string, status int, latency time.Duration, respSize int64) 348 } 349 350 // httpRequestMetricHelperImpl implements HTTPRequestMetricHelper 351 type httpRequestMetricHelperImpl struct { 352 requestTracker *prometheus.CounterVec 353 latencyTracker *prometheus.CounterVec 354 respSizeTracker *prometheus.CounterVec 355 } 356 357 func (t *httpRequestMetricHelperImpl) RecordRequest( 358 method string, status int, latency time.Duration, respSize int64, 359 ) { 360 // Standardize HTTP methods to upper case 361 method = strings.ToUpper(method) 362 // Convert HTTP response status to a enum 363 statusStr := httpRespCodeToMetricLabel(status) 364 365 // Record request 366 t.requestTracker. 367 With(prometheus.Labels{labelNameHTTPMethod: method, labelNameHTTPStatus: statusStr}). 368 Inc() 369 370 // Record request latency 371 t.latencyTracker. 372 With(prometheus.Labels{labelNameHTTPMethod: method, labelNameHTTPStatus: statusStr}). 373 Add(latency.Seconds()) 374 375 // Record response size 376 t.respSizeTracker. 377 With(prometheus.Labels{labelNameHTTPMethod: method, labelNameHTTPStatus: statusStr}). 378 Add(float64(respSize)) 379 } 380 381 // httpRespCodeToMetricLabel helper function to quantize the HTTP response status code into 382 // defined catagories. 383 func httpRespCodeToMetricLabel(status int) string { 384 if status >= http.StatusInternalServerError { 385 return "5XX" 386 } else if status >= http.StatusBadRequest && status < http.StatusInternalServerError { 387 return "4XX" 388 } else if status >= http.StatusMultipleChoices && status < http.StatusBadRequest { 389 return "3XX" 390 } 391 return "2XX" 392 } 393 394 // PubSubMetricHelper PubSub publish and receive metric recording helper agent 395 type PubSubMetricHelper interface { 396 /* 397 RecordPublish record PubSub publish message 398 399 @param topic string - PubSub topic 400 @param successful bool - whether the operation was successful 401 @param payloadLen int64 - publish payload length 402 */ 403 RecordPublish(topic string, successful bool, payloadLen int64) 404 405 /* 406 RecordReceive record PubSub receive message 407 408 @param topic string - PubSub topic 409 @param successful bool - whether the operation was successful 410 @param payloadLen int64 - receive payload length 411 */ 412 RecordReceive(topic string, successful bool, payloadLen int64) 413 } 414 415 type pubsubMetricHelperImpl struct { 416 publishTracker *prometheus.CounterVec 417 publishPayloadTracker *prometheus.CounterVec 418 receiveTracker *prometheus.CounterVec 419 receivePayloadTracker *prometheus.CounterVec 420 } 421 422 func (t *pubsubMetricHelperImpl) RecordPublish(topic string, successful bool, payloadLen int64) { 423 successStr := "true" 424 if !successful { 425 successStr = "false" 426 } 427 t.publishTracker. 428 With(prometheus.Labels{labelNamePubSubTopic: topic, labelNamePubSubSuccess: successStr}). 429 Inc() 430 t.publishPayloadTracker. 431 With(prometheus.Labels{labelNamePubSubTopic: topic, labelNamePubSubSuccess: successStr}). 432 Add(float64(payloadLen)) 433 } 434 435 func (t *pubsubMetricHelperImpl) RecordReceive(topic string, successful bool, payloadLen int64) { 436 successStr := "true" 437 if !successful { 438 successStr = "false" 439 } 440 t.receiveTracker. 441 With(prometheus.Labels{labelNamePubSubTopic: topic, labelNamePubSubSuccess: successStr}). 442 Inc() 443 t.receivePayloadTracker. 444 With(prometheus.Labels{labelNamePubSubTopic: topic, labelNamePubSubSuccess: successStr}). 445 Add(float64(payloadLen)) 446 }