Skip to content

Commit d08b9ca

Browse files
authored
Merge pull request #126 from Lee-Minjea/feat/status_resolver
[proposal] adding configurable status resolver on prometheus middleware
2 parents 84826fa + 1e69e54 commit d08b9ca

File tree

2 files changed

+80
-10
lines changed

2 files changed

+80
-10
lines changed

echoprometheus/prometheus.go

+22-10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ type MiddlewareConfig struct {
7878
// If DoNotUseRequestPathFor404 is true, all 404 responses (due to non-matching route) will have the same `url` label and
7979
// thus won't generate new metrics.
8080
DoNotUseRequestPathFor404 bool
81+
82+
// StatusCodeResolver resolves err & context into http status code. Default is to use context.Response().Status
83+
StatusCodeResolver func(c echo.Context, err error) int
8184
}
8285

8386
type LabelValueFunc func(c echo.Context, err error) string
@@ -167,6 +170,9 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
167170
return opts
168171
}
169172
}
173+
if conf.StatusCodeResolver == nil {
174+
conf.StatusCodeResolver = defaultStatusResolver
175+
}
170176

171177
labelNames, customValuers := createLabels(conf.LabelFuncs)
172178

@@ -257,16 +263,7 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
257263
url = c.Request().URL.Path
258264
}
259265

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-
}
266+
status := conf.StatusCodeResolver(c, err)
270267

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

echoprometheus/prometheus_test.go

+58
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)