Skip to content

Commit ba778c8

Browse files
author
Lee,Minjea
committed
feat: adding configurable status resolver on prometheus middleware
1 parent 84826fa commit ba778c8

File tree

2 files changed

+87
-16
lines changed

2 files changed

+87
-16
lines changed

echoprometheus/prometheus.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ import (
1111
"context"
1212
"errors"
1313
"fmt"
14-
"github.com/labstack/echo/v4"
15-
"github.com/labstack/echo/v4/middleware"
16-
"github.com/labstack/gommon/log"
17-
"github.com/prometheus/client_golang/prometheus"
18-
"github.com/prometheus/client_golang/prometheus/promhttp"
19-
"github.com/prometheus/common/expfmt"
2014
"io"
2115
"net/http"
2216
"sort"
2317
"strconv"
2418
"strings"
2519
"time"
20+
21+
"github.com/labstack/echo/v4"
22+
"github.com/labstack/echo/v4/middleware"
23+
"github.com/labstack/gommon/log"
24+
"github.com/prometheus/client_golang/prometheus"
25+
"github.com/prometheus/client_golang/prometheus/promhttp"
26+
"github.com/prometheus/common/expfmt"
2627
)
2728

2829
const (
@@ -78,6 +79,9 @@ type MiddlewareConfig struct {
7879
// If DoNotUseRequestPathFor404 is true, all 404 responses (due to non-matching route) will have the same `url` label and
7980
// thus won't generate new metrics.
8081
DoNotUseRequestPathFor404 bool
82+
83+
// StatusCodeResolver resolves err & context into http status code. Default is to use context.Response().Status
84+
StatusCodeResolver func(c echo.Context, err error) int
8185
}
8286

8387
type LabelValueFunc func(c echo.Context, err error) string
@@ -167,6 +171,9 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
167171
return opts
168172
}
169173
}
174+
if conf.StatusCodeResolver == nil {
175+
conf.StatusCodeResolver = defaultStatusResolver
176+
}
170177

171178
labelNames, customValuers := createLabels(conf.LabelFuncs)
172179

@@ -257,16 +264,7 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
257264
url = c.Request().URL.Path
258265
}
259266

260-
status := c.Response().Status
261-
if err != nil {
262-
var httpError *echo.HTTPError
263-
if errors.As(err, &httpError) {
264-
status = httpError.Code
265-
}
266-
if status == 0 || status == http.StatusOK {
267-
status = http.StatusInternalServerError
268-
}
269-
}
267+
status := conf.StatusCodeResolver(c, err)
270268

271269
values := make([]string, len(labelNames))
272270
values[0] = strconv.Itoa(status)
@@ -458,3 +456,18 @@ func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer) error
458456
}
459457
return nil
460458
}
459+
460+
// defaultStatusResolver resolves http status code by referencing echo.HTTPError.
461+
func defaultStatusResolver(c echo.Context, err error) int {
462+
status := c.Response().Status
463+
if err != nil {
464+
var httpError *echo.HTTPError
465+
if errors.As(err, &httpError) {
466+
status = httpError.Code
467+
}
468+
if status == 0 || status == http.StatusOK {
469+
status = http.StatusInternalServerError
470+
}
471+
}
472+
return status
473+
}

echoprometheus/prometheus_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,64 @@ func TestMiddlewareConfig_LabelFuncs(t *testing.T) {
161161
assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="overridden_GET",scheme="http",url="/ok"} 1`)
162162
}
163163

164+
func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) {
165+
e := echo.New()
166+
customRegistry := prometheus.NewRegistry()
167+
customResolver := func(c echo.Context, err error) int {
168+
if err == nil {
169+
return c.Response().Status
170+
}
171+
msg := err.Error()
172+
if strings.Contains(msg, "NOT FOUND") {
173+
return http.StatusNotFound
174+
}
175+
if strings.Contains(msg, "NOT Authorized") {
176+
return http.StatusUnauthorized
177+
}
178+
return http.StatusInternalServerError
179+
}
180+
e.Use(NewMiddlewareWithConfig(MiddlewareConfig{
181+
Skipper: func(c echo.Context) bool {
182+
return strings.HasSuffix(c.Path(), "ignore")
183+
},
184+
Subsystem: "myapp",
185+
Registerer: customRegistry,
186+
StatusCodeResolver: customResolver,
187+
}))
188+
e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry}))
189+
190+
e.GET("/handler_for_ok", func(c echo.Context) error {
191+
return c.JSON(http.StatusOK, "OK")
192+
})
193+
e.GET("/handler_for_nok", func(c echo.Context) error {
194+
return c.JSON(http.StatusConflict, "NOK")
195+
})
196+
e.GET("/handler_for_not_found", func(c echo.Context) error {
197+
return errors.New("NOT FOUND")
198+
})
199+
e.GET("/handler_for_not_authorized", func(c echo.Context) error {
200+
return errors.New("NOT Authorized")
201+
})
202+
e.GET("/handler_for_unknown_error", func(c echo.Context) error {
203+
return errors.New("i do not know")
204+
})
205+
206+
assert.Equal(t, http.StatusOK, request(e, "/handler_for_ok"))
207+
assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok"))
208+
assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_found"))
209+
assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_authorized"))
210+
assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_unknown_error"))
211+
212+
body, code := requestBody(e, "/metrics")
213+
assert.Equal(t, http.StatusOK, code)
214+
assert.Contains(t, body, fmt.Sprintf("%s_requests_total", "myapp"))
215+
assert.Contains(t, body, `myapp_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`)
216+
assert.Contains(t, body, `myapp_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 1`)
217+
assert.Contains(t, body, `myapp_requests_total{code="404",host="example.com",method="GET",url="/handler_for_not_found"} 1`)
218+
assert.Contains(t, body, `myapp_requests_total{code="401",host="example.com",method="GET",url="/handler_for_not_authorized"} 1`)
219+
assert.Contains(t, body, `myapp_requests_total{code="500",host="example.com",method="GET",url="/handler_for_unknown_error"} 1`)
220+
}
221+
164222
func TestMiddlewareConfig_HistogramOptsFunc(t *testing.T) {
165223
e := echo.New()
166224
customRegistry := prometheus.NewRegistry()

0 commit comments

Comments
 (0)