Skip to content

Commit 8c4f77a

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

File tree

2 files changed

+92
-19
lines changed

2 files changed

+92
-19
lines changed

echoprometheus/prometheus.go

+30-16
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,10 +79,29 @@ 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 StatusCodeResolveFunc
8185
}
8286

8387
type LabelValueFunc func(c echo.Context, err error) string
8488

89+
type StatusCodeResolveFunc func(c echo.Context, err error) int
90+
91+
var defaultStatusCodeResolveFunc = func(c echo.Context, err error) int {
92+
status := c.Response().Status
93+
if err != nil {
94+
var httpError *echo.HTTPError
95+
if errors.As(err, &httpError) {
96+
status = httpError.Code
97+
}
98+
if status == 0 || status == http.StatusOK {
99+
status = http.StatusInternalServerError
100+
}
101+
}
102+
return status
103+
}
104+
85105
// HandlerConfig contains the configuration for creating HTTP handler for metrics.
86106
type HandlerConfig struct {
87107
// Gatherer sets the prometheus.Gatherer instance the middleware will use when generating the metric endpoint handler.
@@ -167,6 +187,9 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
167187
return opts
168188
}
169189
}
190+
if conf.StatusCodeResolver == nil {
191+
conf.StatusCodeResolver = defaultStatusCodeResolveFunc
192+
}
170193

171194
labelNames, customValuers := createLabels(conf.LabelFuncs)
172195

@@ -257,16 +280,7 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
257280
url = c.Request().URL.Path
258281
}
259282

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

271285
values := make([]string, len(labelNames))
272286
values[0] = strconv.Itoa(status)

echoprometheus/prometheus_test.go

+62-3
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11-
"github.com/labstack/echo/v4"
12-
"github.com/prometheus/client_golang/prometheus"
13-
"github.com/stretchr/testify/assert"
1411
"net/http"
1512
"net/http/httptest"
1613
"strings"
1714
"testing"
1815
"time"
16+
17+
"github.com/labstack/echo/v4"
18+
"github.com/prometheus/client_golang/prometheus"
19+
"github.com/stretchr/testify/assert"
1920
)
2021

2122
func TestCustomRegistryMetrics(t *testing.T) {
@@ -161,6 +162,64 @@ func TestMiddlewareConfig_LabelFuncs(t *testing.T) {
161162
assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="overridden_GET",scheme="http",url="/ok"} 1`)
162163
}
163164

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

0 commit comments

Comments
 (0)