Skip to content

Commit

Permalink
feat: add custom histograms (#70)
Browse files Browse the repository at this point in the history
* feat: add custom histograms

* fix linter

* test: improve coverage
  • Loading branch information
sralloza committed Feb 21, 2024
1 parent 3eb0203 commit 16eff21
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 2 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -76,7 +77,7 @@ func main() {

## Options

### Custom Counters
### Custom counters

Add custom counters to add own values to the metrics

Expand Down Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions prom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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...)
Expand All @@ -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 {
Expand Down
71 changes: 71 additions & 0 deletions prom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 16eff21

Please sign in to comment.