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  }