From 0768194839f52dcf66e216192bd0ee260811b791 Mon Sep 17 00:00:00 2001 From: Christian Jauvin Date: Tue, 24 Oct 2017 11:47:20 -0400 Subject: [PATCH] Supply function hook to control request counter url label cardinality --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++- middleware.go | 29 ++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f2c4223..11e782b 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,56 @@ func main() { } ``` -See the [example.go file](https://github.com/zsais/go-gin-prometheus/blob/master/example/example.go) \ No newline at end of file +See the [example.go file](https://github.com/zsais/go-gin-prometheus/blob/master/example/example.go) + +## Preserving a low cardinality for the request counter + +The request counter (`requests_total`) has a `url` label which, +although desirable, can become problematic in cases where your +application uses templated routes expecting a great number of +variations, as Prometheus explicitly recommends against metrics having +high cardinality dimensions: + +https://prometheus.io/docs/practices/naming/#labels + +If you have for instance a `/customer/:name` templated route and you +don't want to generate a time series for every possible customer name, +you could supply this mapping function to the middleware: + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/zsais/go-gin-prometheus" +) + +func main() { + r := gin.New() + + p := ginprometheus.NewPrometheus("gin") + + p.ReqCntURLLabelMappingFn = func(c *gin.Context) string { + url := c.Request.URL.String() + for _, p := range c.Params { + if p.Key == "name" { + url = strings.Replace(url, p.Value, ":name", 1) + break + } + } + return url + } + + p.Use(r) + + r.GET("/", func(c *gin.Context) { + c.JSON(200, "Hello world!") + }) + + r.Run(":29090") +} +``` + +which would map `/customer/alice` and `/customer/bob` to their +template `/customer/:name`, and thus preserve a low cardinality for +our metrics. diff --git a/middleware.go b/middleware.go index 4c99e7b..a147cd5 100644 --- a/middleware.go +++ b/middleware.go @@ -16,6 +16,27 @@ import ( var defaultMetricPath = "/metrics" +/* +RequestCounterURLLabelMappingFn is a function which can be supplied to the middleware to control +the cardinality of the request counter's "url" label, which might be required in some contexts. +For instance, if for a "/customer/:name" route you don't want to generate a time series for every +possible customer name, you could use this function: + +func(c *gin.Context) string { + url := c.Request.URL.String() + for _, p := range c.Params { + if p.Key == "name" { + url = strings.Replace(url, p.Value, ":name", 1) + break + } + } + return url +} + +which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". +*/ +type RequestCounterURLLabelMappingFn func(c *gin.Context) string + // Prometheus contains the metrics gathered by the instance and its path type Prometheus struct { reqCnt *prometheus.CounterVec @@ -26,6 +47,8 @@ type Prometheus struct { Ppg PrometheusPushGateway MetricsPath string + + ReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn } // PrometheusPushGateway contains the configuration for pushing to a Prometheus pushgateway (optional) @@ -51,6 +74,9 @@ func NewPrometheus(subsystem string) *Prometheus { p := &Prometheus{ MetricsPath: defaultMetricPath, + ReqCntURLLabelMappingFn: func(c *gin.Context) string { + return c.Request.URL.String() // i.e. by default do nothing, i.e. return URL as is + }, } p.registerMetrics(subsystem) @@ -232,7 +258,8 @@ func (p *Prometheus) handlerFunc() gin.HandlerFunc { resSz := float64(c.Writer.Size()) p.reqDur.Observe(elapsed) - p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, c.Request.URL.String()).Inc() + url := p.ReqCntURLLabelMappingFn(c) + p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc() p.reqSz.Observe(float64(reqSz)) p.resSz.Observe(resSz) }