From 16eff21ee826bb2a89b28a9f0d58739b4f2e32e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Alloza=20Gonz=C3=A1lez?= Date: Wed, 21 Feb 2024 19:32:22 +0100 Subject: [PATCH] feat: add custom histograms (#70) * feat: add custom histograms * fix linter * test: improve coverage --- README.md | 22 ++++++++++++++-- prom.go | 34 +++++++++++++++++++++++++ prom_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 596a27f..ba5cadd 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Inspired by [github.com/zsais/go-gin-prometheus](https://github.com/zsais/go-gin - [Differences with go-gin-prometheus](#differences-with-go-gin-prometheus) - [Usage](#usage) - [Options](#options) - - [Custom Counters](#custom-counters) + - [Custom counters](#custom-counters) - [Custom gauges](#custom-gauges) + - [Custom histograms](#custom-histograms) - [Path](#path) - [Namespace](#namespace) - [Subsystem](#subsystem) @@ -76,7 +77,7 @@ func main() { ## Options -### Custom Counters +### Custom counters Add custom counters to add own values to the metrics @@ -113,6 +114,23 @@ Save `p` and use the following functions: - DecrementGaugeValue - SetGaugeValue +### Custom histograms + +Add custom histograms to add own values to the metrics + +```go +r := gin.New() +p := ginprom.New( + ginprom.Engine(r), +) +p.AddCustomHistogram("internal_request_latency", "Duration of internal HTTP requests", []string{"url", "method", "status"}) +r.Use(p.Instrument()) +``` + +Save `p` and use the following functions: + +- AddCustomHistogramValue + ### Path Override the default path (`/metrics`) on which the metrics can be accessed: diff --git a/prom.go b/prom.go index 24285aa..8b44f31 100644 --- a/prom.go +++ b/prom.go @@ -51,6 +51,11 @@ type pmapCounter struct { values map[string]prometheus.CounterVec } +type pmapHistogram struct { + sync.RWMutex + values map[string]prometheus.HistogramVec +} + // Prometheus contains the metrics gathered by the instance and its path. type Prometheus struct { reqCnt *prometheus.CounterVec @@ -61,6 +66,7 @@ type Prometheus struct { customCounters pmapCounter customCounterLabelsProvider func(c *gin.Context) map[string]string customCounterLabels []string + customHistograms pmapHistogram MetricsPath string Namespace string @@ -201,6 +207,33 @@ func (p *Prometheus) AddCustomCounter(name, help string, labels []string) { p.mustRegister(g) } +// AddCustomHistogramValue adds value to custom counter. +func (p *Prometheus) AddCustomHistogramValue(name string, labelValues []string, value float64) error { + p.customHistograms.RLock() + defer p.customHistograms.RUnlock() + + if g, ok := p.customHistograms.values[name]; ok { + g.WithLabelValues(labelValues...).Observe(value) + } else { + return ErrCustomCounter + } + return nil +} + +// AddCustomCounter adds a custom counter and registers it. +func (p *Prometheus) AddCustomHistogram(name, help string, labels []string) { + p.customHistograms.Lock() + defer p.customHistograms.Unlock() + g := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: p.Namespace, + Subsystem: p.Subsystem, + Name: name, + Help: help, + }, labels) + p.customHistograms.values[name] = *g + p.mustRegister(g) +} + func (p *Prometheus) mustRegister(c ...prometheus.Collector) { registerer, _ := p.getRegistererAndGatherer() registerer.MustRegister(c...) @@ -225,6 +258,7 @@ func New(options ...PrometheusOption) *Prometheus { p.customGauges.values = make(map[string]prometheus.GaugeVec) p.customCounters.values = make(map[string]prometheus.CounterVec) p.customCounterLabels = make([]string, 0) + p.customHistograms.values = make(map[string]prometheus.HistogramVec) p.Ignored.values = make(map[string]bool) for _, option := range options { diff --git a/prom_test.go b/prom_test.go index 8e537c5..458db01 100644 --- a/prom_test.go +++ b/prom_test.go @@ -424,6 +424,77 @@ func TestCustomCounterMetrics(t *testing.T) { unregister(p) } +func TestCustomHistogram(t *testing.T) { + r := gin.New() + p := New(Engine(r), Registry(prometheus.NewRegistry())) + p.AddCustomHistogram("request_latency", "test histogram", []string{"url", "method"}) + r.Use(p.Instrument()) + defer unregister(p) + + r.GET("/ping", func(c *gin.Context) { + err := p.AddCustomHistogramValue("request_latency", []string{"http://example.com/status", "GET"}, 0.45) + assert.NoError(t, err) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + r.GET("/pong", func(c *gin.Context) { + err := p.AddCustomHistogramValue("request_latency", []string{"http://example.com/status", "GET"}, 9.56) + assert.NoError(t, err) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + r.GET("/error", func(c *gin.Context) { + // Metric not found + err := p.AddCustomHistogramValue("invalid", []string{}, 9.56) + assert.Error(t, err) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + expectedLines := []string{ + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.005"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.01"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.025"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.05"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.1"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.25"} 0`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.5"} 1`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="1"} 1`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="2.5"} 1`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="5"} 1`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="10"} 2`, + `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="+Inf"} 2`, + `gin_gonic_request_latency_sum{method="GET",url="http://example.com/status"} 10.01`, + `gin_gonic_request_latency_count{method="GET",url="http://example.com/status"} 2`, + } + + g := gofight.New() + g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { + assert.Equal(t, http.StatusOK, r.Code) + assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "request_latency")) + + for _, line := range expectedLines { + assert.NotContains(t, r.Body.String(), line) + } + }) + + g.GET("/ping").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { + assert.Equal(t, http.StatusOK, r.Code) + }) + g.GET("/pong").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { + assert.Equal(t, http.StatusOK, r.Code) + }) + g.GET("/error").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { + assert.Equal(t, http.StatusOK, r.Code) + }) + + g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { + assert.Equal(t, http.StatusOK, r.Code) + assert.Contains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "request_latency")) + + for _, line := range expectedLines { + assert.Contains(t, r.Body.String(), line) + } + }) +} + func TestIgnore(t *testing.T) { r := gin.New() ipath := "/ping"