Skip to content

Commit bd19a9a

Browse files
franciscocpgclaude
andauthored
feat: add HostFunc option to customize host metric label (#143)
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 <noreply@anthropic.com>
1 parent 485a2eb commit bd19a9a

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

options.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ func HandlerNameFunc(f func(c *gin.Context) string) PrometheusOption {
124124
}
125125
}
126126

127+
// HostFunc is an option allowing to set the HostFunc with New.
128+
// Use this option if you want to override the default behavior (i.e. using
129+
// c.Request.Host). This is useful to reduce metric cardinality when the Host
130+
// header varies (e.g. dynamic subdomains) but requests are handled the same.
131+
// Example:
132+
// r := gin.Default()
133+
// p := ginprom.New(HostFunc(func(c *gin.Context) string { return "my-service" }))
134+
func HostFunc(f func(c *gin.Context) string) PrometheusOption {
135+
return func(p *Prometheus) {
136+
p.HostFunc = f
137+
}
138+
}
139+
127140
// HandlerOpts is an option allowing to set the promhttp.HandlerOpts.
128141
// Use this option if you want to override the default zero value.
129142
func HandlerOpts(opts promhttp.HandlerOpts) PrometheusOption {

prom.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var defaultNs = "gin"
2121
var defaultSys = "gonic"
2222
var defaultHandlerNameFunc = (*gin.Context).HandlerName
2323
var defaultRequestPathFunc = (*gin.Context).FullPath
24+
var defaultHostFunc = func(c *gin.Context) string { return c.Request.Host }
2425

2526
var defaultReqCntMetricName = "requests_total"
2627
var defaultReqDurMetricName = "request_duration"
@@ -79,6 +80,7 @@ type Prometheus struct {
7980
Registry *prometheus.Registry
8081
HandlerNameFunc func(c *gin.Context) string
8182
RequestPathFunc func(c *gin.Context) string
83+
HostFunc func(c *gin.Context) string
8284
HandlerOpts promhttp.HandlerOpts
8385

8486
NativeHistogramBucketFactor float64
@@ -272,6 +274,7 @@ func New(options ...PrometheusOption) *Prometheus {
272274
Subsystem: defaultSys,
273275
HandlerNameFunc: defaultHandlerNameFunc,
274276
RequestPathFunc: defaultRequestPathFunc,
277+
HostFunc: defaultHostFunc,
275278
RequestCounterMetricName: defaultReqCntMetricName,
276279
RequestDurationMetricName: defaultReqDurMetricName,
277280
RequestSizeMetricName: defaultReqSzMetricName,
@@ -392,7 +395,8 @@ func (p *Prometheus) Instrument() gin.HandlerFunc {
392395
elapsed := float64(time.Since(start)) / float64(time.Second)
393396
resSz := float64(c.Writer.Size())
394397

395-
labels := []string{status, c.Request.Method, p.HandlerNameFunc(c), c.Request.Host, path}
398+
host := p.HostFunc(c)
399+
labels := []string{status, c.Request.Method, p.HandlerNameFunc(c), host, path}
396400
if p.customCounterLabelsProvider != nil {
397401
extraLabels := p.customCounterLabelsProvider(c)
398402
for _, label := range p.customCounterLabels {
@@ -401,7 +405,7 @@ func (p *Prometheus) Instrument() gin.HandlerFunc {
401405
}
402406

403407
p.reqCnt.WithLabelValues(labels...).Inc()
404-
p.reqDur.WithLabelValues(c.Request.Method, path, c.Request.Host).Observe(elapsed)
408+
p.reqDur.WithLabelValues(c.Request.Method, path, host).Observe(elapsed)
405409
p.reqSz.Observe(float64(reqSz))
406410
p.resSz.Observe(resSz)
407411
}

prom_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,38 @@ func TestHandlerNameFunc(t *testing.T) {
124124
})
125125
}
126126

127+
func TestHostFunc(t *testing.T) {
128+
r := gin.New()
129+
registry := prometheus.NewRegistry()
130+
host := "normalized.example.com"
131+
lhost := fmt.Sprintf("host=%q", host)
132+
133+
p := New(
134+
HostFunc(func(c *gin.Context) string {
135+
return host
136+
}),
137+
Registry(registry),
138+
Engine(r),
139+
)
140+
141+
r.Use(p.Instrument())
142+
143+
r.GET("/", func(context *gin.Context) {
144+
context.Status(http.StatusOK)
145+
})
146+
147+
g := gofight.New()
148+
149+
g.GET("/").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) {
150+
assert.Equal(t, response.Code, http.StatusOK)
151+
})
152+
153+
g.GET(p.MetricsPath).Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) {
154+
assert.Equal(t, response.Code, http.StatusOK)
155+
assert.Contains(t, response.Body.String(), lhost)
156+
})
157+
}
158+
127159
func TestHandlerOpts(t *testing.T) {
128160
r := gin.New()
129161
registry := prometheus.NewRegistry()

0 commit comments

Comments
 (0)