Skip to content

Commit

Permalink
Add new RequestPathFunc feature to customize request path (#41)
Browse files Browse the repository at this point in the history
* feat: new RequestPathFunc to retrieve request path and corresponding option on *Prometheus

Recording metrics for routes not registered on the instrumented root *gin.Router currently requires defining a side-middleware mimicking the *Prometheus internals, that is, declaring counters, histograms and summaries for the metrics one wants to record, and "manually" handling them inside the middleware.
With RequestPathFunc, one can monitor calls to unregistered routes by setting RequestPathFunc to some function that returns a default value if the request path is undefined.

Even finer-grained monitoring is made possible by this commit, e.g. recording a metric with the (*http.Request).RequestURI rather than the (*gin.Context).Fullpath() return value.

* feat: add test for new feature RequestPathFunc
  • Loading branch information
mlevieux authored Apr 18, 2023
1 parent 867e278 commit 9f1d545
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 1 deletion.
27 changes: 26 additions & 1 deletion prom.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var defaultPath = "/metrics"
var defaultNs = "gin"
var defaultSys = "gonic"
var defaultHandlerNameFunc = (*gin.Context).HandlerName
var defaultRequestPathFunc = (*gin.Context).FullPath

var defaultReqCntMetricName = "requests_total"
var defaultReqDurMetricName = "request_duration"
Expand Down Expand Up @@ -68,6 +69,7 @@ type Prometheus struct {
BucketsSize []float64
Registry *prometheus.Registry
HandlerNameFunc func(c *gin.Context) string
RequestPathFunc func(c *gin.Context) string

RequestCounterMetricName string
RequestDurationMetricName string
Expand Down Expand Up @@ -315,6 +317,28 @@ func HandlerNameFunc(f func(c *gin.Context) string) func(*Prometheus) {
}
}

// RequestPathFunc is an option allowing to set the RequestPathFunc with New.
// Use this option if you want to override the default behavior (i.e. using
// (*gin.Context).FullPath). This is useful when wanting to group different requests
// under the same "path" label or when wanting to process unknown routes (the default
// (*gin.Context).FullPath return an empty string for unregistered routes). Note that
// requests for which f returns the empty string are ignored.
// To specifically ignore certain paths, see the Ignore option.
// Example:
//
// r := gin.Default()
// p := ginprom.New(RequestPathFunc(func (c *gin.Context) string {
// if fullpath := c.FullPath(); fullpath != "" {
// return fullpath
// }
// return "<unknown>"
// }))
func RequestPathFunc(f func(c *gin.Context) string) func(*Prometheus) {
return func(p *Prometheus) {
p.RequestPathFunc = f
}
}

// New will initialize a new Prometheus instance with the given options.
// If no options are passed, sane defaults are used.
// If a router is passed using the Engine() option, this instance will
Expand All @@ -325,6 +349,7 @@ func New(options ...func(*Prometheus)) *Prometheus {
Namespace: defaultNs,
Subsystem: defaultSys,
HandlerNameFunc: defaultHandlerNameFunc,
RequestPathFunc: defaultRequestPathFunc,
RequestCounterMetricName: defaultReqCntMetricName,
RequestDurationMetricName: defaultReqDurMetricName,
RequestSizeMetricName: defaultReqSzMetricName,
Expand Down Expand Up @@ -408,7 +433,7 @@ func (p *Prometheus) isIgnored(path string) bool {
func (p *Prometheus) Instrument() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.FullPath()
path := p.RequestPathFunc(c)

if path == "" || p.isIgnored(path) {
c.Next()
Expand Down
39 changes: 39 additions & 0 deletions prom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,45 @@ func TestHandlerNameFunc(t *testing.T) {
})
}

func TestRequestPathFunc(t *testing.T) {
r := gin.New()
registry := prometheus.NewRegistry()

correctPath := fmt.Sprintf("path=%q", "/some/path")
unknownPath := fmt.Sprintf("path=%q", "<unknown>")

p := New(
RequestPathFunc(func(c *gin.Context) string {
if fullpath := c.FullPath(); fullpath != "" {
return fullpath
}
return "<unknown>"
}),
Engine(r),
Registry(registry),
)

r.Use(p.Instrument())

r.GET("/some/path", func(context *gin.Context) {
context.Status(http.StatusOK)
})

g := gofight.New()
g.GET("/some/path").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) {
assert.Equal(t, response.Code, http.StatusOK)
})
g.GET("/some/other/path").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) {
assert.Equal(t, response.Code, http.StatusNotFound)
})

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(), correctPath)
assert.Contains(t, response.Body.String(), unknownPath)
})
}

func TestNamespace(t *testing.T) {
p := New()
assert.Equal(t, p.Namespace, defaultNs, "namespace should be default")
Expand Down

0 comments on commit 9f1d545

Please sign in to comment.