From 990b1457f70b530017648b80a2df5fc2ab92a2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= Date: Wed, 11 Feb 2026 16:28:49 -0300 Subject: [PATCH] feat: add HostFunc option to customize host metric label Allow users to override the host value used in Prometheus metric labels (counter and histogram) to reduce cardinality when the Host header varies (e.g. dynamic subdomains). When not set, defaults to c.Request.Host (preserving current behavior). Co-Authored-By: Claude Opus 4.6 --- options.go | 13 +++++++++++++ prom.go | 8 ++++++-- prom_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/options.go b/options.go index 4078350..b193d1c 100644 --- a/options.go +++ b/options.go @@ -124,6 +124,19 @@ func HandlerNameFunc(f func(c *gin.Context) string) PrometheusOption { } } +// HostFunc is an option allowing to set the HostFunc with New. +// Use this option if you want to override the default behavior (i.e. using +// c.Request.Host). This is useful to reduce metric cardinality when the Host +// header varies (e.g. dynamic subdomains) but requests are handled the same. +// Example: +// r := gin.Default() +// p := ginprom.New(HostFunc(func(c *gin.Context) string { return "my-service" })) +func HostFunc(f func(c *gin.Context) string) PrometheusOption { + return func(p *Prometheus) { + p.HostFunc = f + } +} + // HandlerOpts is an option allowing to set the promhttp.HandlerOpts. // Use this option if you want to override the default zero value. func HandlerOpts(opts promhttp.HandlerOpts) PrometheusOption { diff --git a/prom.go b/prom.go index b8713bc..cbba91b 100644 --- a/prom.go +++ b/prom.go @@ -21,6 +21,7 @@ var defaultNs = "gin" var defaultSys = "gonic" var defaultHandlerNameFunc = (*gin.Context).HandlerName var defaultRequestPathFunc = (*gin.Context).FullPath +var defaultHostFunc = func(c *gin.Context) string { return c.Request.Host } var defaultReqCntMetricName = "requests_total" var defaultReqDurMetricName = "request_duration" @@ -79,6 +80,7 @@ type Prometheus struct { Registry *prometheus.Registry HandlerNameFunc func(c *gin.Context) string RequestPathFunc func(c *gin.Context) string + HostFunc func(c *gin.Context) string HandlerOpts promhttp.HandlerOpts NativeHistogramBucketFactor float64 @@ -272,6 +274,7 @@ func New(options ...PrometheusOption) *Prometheus { Subsystem: defaultSys, HandlerNameFunc: defaultHandlerNameFunc, RequestPathFunc: defaultRequestPathFunc, + HostFunc: defaultHostFunc, RequestCounterMetricName: defaultReqCntMetricName, RequestDurationMetricName: defaultReqDurMetricName, RequestSizeMetricName: defaultReqSzMetricName, @@ -392,7 +395,8 @@ func (p *Prometheus) Instrument() gin.HandlerFunc { elapsed := float64(time.Since(start)) / float64(time.Second) resSz := float64(c.Writer.Size()) - labels := []string{status, c.Request.Method, p.HandlerNameFunc(c), c.Request.Host, path} + host := p.HostFunc(c) + labels := []string{status, c.Request.Method, p.HandlerNameFunc(c), host, path} if p.customCounterLabelsProvider != nil { extraLabels := p.customCounterLabelsProvider(c) for _, label := range p.customCounterLabels { @@ -401,7 +405,7 @@ func (p *Prometheus) Instrument() gin.HandlerFunc { } p.reqCnt.WithLabelValues(labels...).Inc() - p.reqDur.WithLabelValues(c.Request.Method, path, c.Request.Host).Observe(elapsed) + p.reqDur.WithLabelValues(c.Request.Method, path, host).Observe(elapsed) p.reqSz.Observe(float64(reqSz)) p.resSz.Observe(resSz) } diff --git a/prom_test.go b/prom_test.go index 1d9dbb2..627a5e4 100644 --- a/prom_test.go +++ b/prom_test.go @@ -124,6 +124,38 @@ func TestHandlerNameFunc(t *testing.T) { }) } +func TestHostFunc(t *testing.T) { + r := gin.New() + registry := prometheus.NewRegistry() + host := "normalized.example.com" + lhost := fmt.Sprintf("host=%q", host) + + p := New( + HostFunc(func(c *gin.Context) string { + return host + }), + Registry(registry), + Engine(r), + ) + + r.Use(p.Instrument()) + + r.GET("/", func(context *gin.Context) { + context.Status(http.StatusOK) + }) + + g := gofight.New() + + g.GET("/").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { + assert.Equal(t, response.Code, http.StatusOK) + }) + + g.GET(p.MetricsPath).Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { + assert.Equal(t, response.Code, http.StatusOK) + assert.Contains(t, response.Body.String(), lhost) + }) +} + func TestHandlerOpts(t *testing.T) { r := gin.New() registry := prometheus.NewRegistry()