diff --git a/.github/workflows/echo-contrib.yml b/.github/workflows/echo-contrib.yml index be40301..9e18cf6 100644 --- a/.github/workflows/echo-contrib.yml +++ b/.github/workflows/echo-contrib.yml @@ -25,7 +25,7 @@ jobs: # Echo CORE tests with last four major releases (unless there are pressing vulnerabilities) # As we depend on MANY DIFFERENT libraries which of SOME support last 2 Go releases we could have situations when # we derive from last four major releases promise. - go: ["1.24", "1.25"] + go: ["1.25"] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: diff --git a/casbin/casbin.go b/casbin/casbin.go index c56fd01..cd832d8 100644 --- a/casbin/casbin.go +++ b/casbin/casbin.go @@ -9,7 +9,7 @@ Simple example: import ( "github.com/casbin/casbin/v2" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" casbin_mw "github.com/labstack/echo-contrib/casbin" ) @@ -28,7 +28,7 @@ Advanced example: import ( "github.com/casbin/casbin/v2" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" casbin_mw "github.com/labstack/echo-contrib/casbin" ) @@ -49,10 +49,11 @@ package casbin import ( "errors" - "github.com/casbin/casbin/v2" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "net/http" + + "github.com/casbin/casbin/v2" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) type ( @@ -67,13 +68,13 @@ type ( // EnforceHandler is custom callback to handle enforcing. // One of Enforcer or EnforceHandler fields is required. - EnforceHandler func(c echo.Context, user string) (bool, error) + EnforceHandler func(c *echo.Context, user string) (bool, error) // Method to get the username - defaults to using basic auth - UserGetter func(c echo.Context) (string, error) + UserGetter func(c *echo.Context) (string, error) // Method to handle errors - ErrorHandler func(c echo.Context, internal error, proposedStatus int) error + ErrorHandler func(c *echo.Context, internal error, proposedStatus int) error } ) @@ -81,14 +82,12 @@ var ( // DefaultConfig is the default CasbinAuth middleware config. DefaultConfig = Config{ Skipper: middleware.DefaultSkipper, - UserGetter: func(c echo.Context) (string, error) { + UserGetter: func(c *echo.Context) (string, error) { username, _, _ := c.Request().BasicAuth() return username, nil }, - ErrorHandler: func(c echo.Context, internal error, proposedStatus int) error { - err := echo.NewHTTPError(proposedStatus, internal.Error()) - err.Internal = internal - return err + ErrorHandler: func(c *echo.Context, internal error, proposedStatus int) error { + return echo.NewHTTPError(proposedStatus, internal.Error()).Wrap(internal) }, } ) @@ -119,13 +118,13 @@ func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { config.ErrorHandler = DefaultConfig.ErrorHandler } if config.EnforceHandler == nil { - config.EnforceHandler = func(c echo.Context, user string) (bool, error) { + config.EnforceHandler = func(c *echo.Context, user string) (bool, error) { return config.Enforcer.Enforce(user, c.Request().URL.Path, c.Request().Method) } } return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } diff --git a/casbin/casbin_test.go b/casbin/casbin_test.go index 69227be..6826824 100644 --- a/casbin/casbin_test.go +++ b/casbin/casbin_test.go @@ -5,15 +5,16 @@ package casbin import ( "errors" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/casbin/casbin/v2" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func testRequest(t *testing.T, h echo.HandlerFunc, user string, path string, method string, code int) { @@ -33,77 +34,81 @@ func testRequest(t *testing.T, h echo.HandlerFunc, user string, path string, met } } } else { - if c.Response().Status != code { - t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response().Status, code) + status := 0 + if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + status = eResp.Status + } + if status != code { + t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, status, code) } } } func TestAuth(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") - h := Middleware(ce)(func(c echo.Context) error { + h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "alice", "/dataset1/resource1", echo.GET, http.StatusOK) - testRequest(t, h, "alice", "/dataset1/resource1", echo.POST, http.StatusOK) - testRequest(t, h, "alice", "/dataset1/resource2", echo.GET, http.StatusOK) - testRequest(t, h, "alice", "/dataset1/resource2", echo.POST, http.StatusForbidden) + testRequest(t, h, "alice", "/dataset1/resource1", http.MethodGet, http.StatusOK) + testRequest(t, h, "alice", "/dataset1/resource1", http.MethodPost, http.StatusOK) + testRequest(t, h, "alice", "/dataset1/resource2", http.MethodGet, http.StatusOK) + testRequest(t, h, "alice", "/dataset1/resource2", http.MethodPost, http.StatusForbidden) } func TestPathWildcard(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") - h := Middleware(ce)(func(c echo.Context) error { + h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "bob", "/dataset2/resource1", echo.GET, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/resource1", echo.POST, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/resource1", echo.DELETE, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/resource2", echo.GET, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/resource2", echo.POST, http.StatusForbidden) - testRequest(t, h, "bob", "/dataset2/resource2", echo.DELETE, http.StatusForbidden) - - testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.GET, http.StatusForbidden) - testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.POST, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.DELETE, http.StatusForbidden) - testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.GET, http.StatusForbidden) - testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.POST, http.StatusOK) - testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.DELETE, http.StatusForbidden) + testRequest(t, h, "bob", "/dataset2/resource1", http.MethodGet, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/resource1", http.MethodPost, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/resource1", http.MethodDelete, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/resource2", http.MethodGet, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/resource2", http.MethodPost, http.StatusForbidden) + testRequest(t, h, "bob", "/dataset2/resource2", http.MethodDelete, http.StatusForbidden) + + testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodGet, http.StatusForbidden) + testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodPost, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodDelete, http.StatusForbidden) + testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodGet, http.StatusForbidden) + testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodPost, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodDelete, http.StatusForbidden) } func TestRBAC(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") - h := Middleware(ce)(func(c echo.Context) error { + h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) // cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role. - testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusOK) - testRequest(t, h, "cathy", "/dataset1/item", echo.POST, http.StatusOK) - testRequest(t, h, "cathy", "/dataset1/item", echo.DELETE, http.StatusOK) - testRequest(t, h, "cathy", "/dataset2/item", echo.GET, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset2/item", echo.POST, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset2/item", echo.DELETE, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusOK) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodPost, http.StatusOK) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodDelete, http.StatusOK) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodGet, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodPost, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodDelete, http.StatusForbidden) // delete all roles on user cathy, so cathy cannot access any resources now. ce.DeleteRolesForUser("cathy") - testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset1/item", echo.POST, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset1/item", echo.DELETE, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset2/item", echo.GET, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset2/item", echo.POST, http.StatusForbidden) - testRequest(t, h, "cathy", "/dataset2/item", echo.DELETE, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodPost, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodDelete, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodGet, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodPost, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset2/item", http.MethodDelete, http.StatusForbidden) } func TestEnforceError(t *testing.T) { ce, _ := casbin.NewEnforcer("broken_auth_model.conf", "auth_policy.csv") - h := Middleware(ce)(func(c echo.Context) error { + h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusInternalServerError) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusInternalServerError) } func TestCustomUserGetter(t *testing.T) { @@ -111,14 +116,14 @@ func TestCustomUserGetter(t *testing.T) { cnf := Config{ Skipper: middleware.DefaultSkipper, Enforcer: ce, - UserGetter: func(c echo.Context) (string, error) { + UserGetter: func(c *echo.Context) (string, error) { return "not_cathy_at_all", nil }, } - h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { + h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) } func TestUserGetterError(t *testing.T) { @@ -126,14 +131,14 @@ func TestUserGetterError(t *testing.T) { cnf := Config{ Skipper: middleware.DefaultSkipper, Enforcer: ce, - UserGetter: func(c echo.Context) (string, error) { + UserGetter: func(c *echo.Context) (string, error) { return "", errors.New("no idea who you are") }, } - h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { + h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) + testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) } func TestCustomEnforceHandler(t *testing.T) { @@ -144,7 +149,7 @@ func TestCustomEnforceHandler(t *testing.T) { assert.NoError(t, err) cnf := Config{ - EnforceHandler: func(c echo.Context, user string) (bool, error) { + EnforceHandler: func(c *echo.Context, user string) (bool, error) { method := c.Request().Method if strings.HasPrefix(c.Request().URL.Path, "/user/bob") { method += "_SELF" @@ -152,25 +157,25 @@ func TestCustomEnforceHandler(t *testing.T) { return ce.Enforce(user, c.Request().URL.Path, method) }, } - h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { + h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "bob", "/dataset2/resource1", echo.GET, http.StatusOK) - testRequest(t, h, "bob", "/user/alice", echo.PATCH, http.StatusForbidden) - testRequest(t, h, "bob", "/user/bob", echo.PATCH, http.StatusOK) + testRequest(t, h, "bob", "/dataset2/resource1", http.MethodGet, http.StatusOK) + testRequest(t, h, "bob", "/user/alice", http.MethodPatch, http.StatusForbidden) + testRequest(t, h, "bob", "/user/bob", http.MethodPatch, http.StatusOK) } func TestCustomSkipper(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") cnf := Config{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return c.Request().URL.Path == "/dataset1/resource1" }, Enforcer: ce, } - h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { + h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) - testRequest(t, h, "alice", "/dataset1/resource1", echo.GET, http.StatusOK) - testRequest(t, h, "alice", "/dataset1/resource2", echo.POST, http.StatusForbidden) + testRequest(t, h, "alice", "/dataset1/resource1", http.MethodGet, http.StatusOK) + testRequest(t, h, "alice", "/dataset1/resource2", http.MethodPost, http.StatusForbidden) } diff --git a/echoprometheus/prometheus.go b/echoprometheus/prometheus.go index 7cab72c..1398be6 100644 --- a/echoprometheus/prometheus.go +++ b/echoprometheus/prometheus.go @@ -11,18 +11,19 @@ import ( "context" "errors" "fmt" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/log" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/prometheus/common/expfmt" "io" "net/http" "sort" "strconv" "strings" "time" + + "github.com/labstack/echo-contrib/internal/helpers" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/expfmt" ) const ( @@ -67,11 +68,11 @@ type MiddlewareConfig struct { // BeforeNext is callback that is executed before next middleware/handler is called. Useful for case when you have own // metrics that need data to be stored for AfterNext. - BeforeNext func(c echo.Context) + BeforeNext func(c *echo.Context) // AfterNext is callback that is executed after next middleware/handler returns. Useful for case when you have own // metrics that need incremented/observed. - AfterNext func(c echo.Context, err error) + AfterNext func(c *echo.Context, err error) timeNow func() time.Time @@ -80,10 +81,10 @@ type MiddlewareConfig struct { DoNotUseRequestPathFor404 bool // StatusCodeResolver resolves err & context into http status code. Default is to use context.Response().Status - StatusCodeResolver func(c echo.Context, err error) int + StatusCodeResolver func(c *echo.Context, err error) int } -type LabelValueFunc func(c echo.Context, err error) string +type LabelValueFunc func(c *echo.Context, err error) string // HandlerConfig contains the configuration for creating HTTP handler for metrics. type HandlerConfig struct { @@ -129,7 +130,7 @@ func NewHandlerWithConfig(config HandlerConfig) echo.HandlerFunc { h = promhttp.InstrumentMetricHandler(r, h) } - return func(c echo.Context) error { + return func(c *echo.Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } @@ -171,7 +172,7 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { } } if conf.StatusCodeResolver == nil { - conf.StatusCodeResolver = defaultStatusResolver + conf.StatusCodeResolver = helpers.DefaultStatusResolver } labelNames, customValuers := createLabels(conf.LabelFuncs) @@ -203,8 +204,8 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { }), labelNames, ) - if err := conf.Registerer.Register(requestDuration); err != nil { - return nil, err + if rErr := conf.Registerer.Register(requestDuration); rErr != nil { + return nil, rErr } responseSize := prometheus.NewHistogramVec( @@ -231,12 +232,12 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { }), labelNames, ) - if err := conf.Registerer.Register(requestSize); err != nil { - return nil, err + if rErr := conf.Registerer.Register(requestSize); rErr != nil { + return nil, rErr } return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // NB: we do not skip metrics handler path by default. This can be added with custom Skipper but for default // behaviour we measure metrics path request/response metrics also if conf.Skipper != nil && conf.Skipper(c) { @@ -289,7 +290,9 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { return fmt.Errorf("failed to label request size metric with values, err: %w", err) } if obs, err := responseSize.GetMetricWithLabelValues(values...); err == nil { - obs.Observe(float64(c.Response().Size)) + if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + obs.Observe(float64(eResp.Size)) + } } else { return fmt.Errorf("failed to label response size metric with values, err: %w", err) } @@ -395,7 +398,6 @@ func RunPushGatewayGatherer(ctx context.Context, config PushGatewayConfig) error } if config.ErrorHandler == nil { config.ErrorHandler = func(err error) error { - log.Error(err) return nil } } @@ -455,18 +457,3 @@ func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer) error } return nil } - -// defaultStatusResolver resolves http status code by referencing echo.HTTPError. -func defaultStatusResolver(c echo.Context, err error) int { - status := c.Response().Status - if err != nil { - var httpError *echo.HTTPError - if errors.As(err, &httpError) { - status = httpError.Code - } - if status == 0 || status == http.StatusOK { - status = http.StatusInternalServerError - } - } - return status -} diff --git a/echoprometheus/prometheus_test.go b/echoprometheus/prometheus_test.go index 9edd012..c0333d4 100644 --- a/echoprometheus/prometheus_test.go +++ b/echoprometheus/prometheus_test.go @@ -8,14 +8,15 @@ import ( "context" "errors" "fmt" - "github.com/labstack/echo/v4" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "strings" "testing" "time" + + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" ) func TestCustomRegistryMetrics(t *testing.T) { @@ -71,17 +72,17 @@ func TestMiddlewareConfig_Skipper(t *testing.T) { customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { hasSuffix := strings.HasSuffix(c.Path(), "ignore") return hasSuffix }, Registerer: customRegistry, })) - e.GET("/test", func(c echo.Context) error { + e.GET("/test", func(c *echo.Context) error { return c.String(http.StatusOK, "OK") }) - e.GET("/test_ignore", func(c echo.Context) error { + e.GET("/test_ignore", func(c *echo.Context) error { return c.String(http.StatusOK, "OK") }) @@ -103,7 +104,7 @@ func TestMetricsForErrors(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return strings.HasSuffix(c.Path(), "ignore") }, Subsystem: "myapp", @@ -111,13 +112,13 @@ func TestMetricsForErrors(t *testing.T) { })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/handler_for_ok", func(c echo.Context) error { + e.GET("/handler_for_ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) - e.GET("/handler_for_nok", func(c echo.Context) error { + e.GET("/handler_for_nok", func(c *echo.Context) error { return c.JSON(http.StatusConflict, "NOK") }) - e.GET("/handler_for_error", func(c echo.Context) error { + e.GET("/handler_for_error", func(c *echo.Context) error { return echo.NewHTTPError(http.StatusBadGateway, "BAD") }) @@ -139,10 +140,10 @@ func TestMiddlewareConfig_LabelFuncs(t *testing.T) { customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ LabelFuncs: map[string]LabelValueFunc{ - "scheme": func(c echo.Context, err error) string { // additional custom label + "scheme": func(c *echo.Context, err error) string { // additional custom label return c.Scheme() }, - "method": func(c echo.Context, err error) string { // overrides default 'method' label value + "method": func(c *echo.Context, err error) string { // overrides default 'method' label value return "overridden_" + c.Request().Method }, }, @@ -150,7 +151,7 @@ func TestMiddlewareConfig_LabelFuncs(t *testing.T) { })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/ok", func(c echo.Context) error { + e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) @@ -164,9 +165,12 @@ func TestMiddlewareConfig_LabelFuncs(t *testing.T) { func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() - customResolver := func(c echo.Context, err error) int { + customResolver := func(c *echo.Context, err error) int { if err == nil { - return c.Response().Status + if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + return eResp.Status + } + return http.StatusOK } msg := err.Error() if strings.Contains(msg, "NOT FOUND") { @@ -178,7 +182,7 @@ func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) { return http.StatusInternalServerError } e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return strings.HasSuffix(c.Path(), "ignore") }, Subsystem: "myapp", @@ -187,19 +191,19 @@ func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) { })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/handler_for_ok", func(c echo.Context) error { + e.GET("/handler_for_ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) - e.GET("/handler_for_nok", func(c echo.Context) error { + e.GET("/handler_for_nok", func(c *echo.Context) error { return c.JSON(http.StatusConflict, "NOK") }) - e.GET("/handler_for_not_found", func(c echo.Context) error { + e.GET("/handler_for_not_found", func(c *echo.Context) error { return errors.New("NOT FOUND") }) - e.GET("/handler_for_not_authorized", func(c echo.Context) error { + e.GET("/handler_for_not_authorized", func(c *echo.Context) error { return errors.New("NOT Authorized") }) - e.GET("/handler_for_unknown_error", func(c echo.Context) error { + e.GET("/handler_for_unknown_error", func(c *echo.Context) error { return errors.New("i do not know") }) @@ -233,7 +237,7 @@ func TestMiddlewareConfig_HistogramOptsFunc(t *testing.T) { })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/ok", func(c echo.Context) error { + e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) @@ -262,7 +266,7 @@ func TestMiddlewareConfig_CounterOptsFunc(t *testing.T) { })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/ok", func(c echo.Context) error { + e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) @@ -292,14 +296,14 @@ func TestMiddlewareConfig_AfterNextFuncs(t *testing.T) { } e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ - AfterNext: func(c echo.Context, err error) { + AfterNext: func(c *echo.Context, err error) { customCounter.Inc() // use our custom metric in middleware }, Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) - e.GET("/ok", func(c echo.Context) error { + e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) @@ -362,7 +366,9 @@ func TestSetPathFor404Logic(t *testing.T) { e.Use(NewMiddlewareWithConfig(MiddlewareConfig{DoNotUseRequestPathFor404: true, Subsystem: defaultSubsystem})) e.GET("/metrics", NewHandler()) - e.GET("/sample", echo.NotFoundHandler) + e.GET("/sample", func(c *echo.Context) error { + return echo.ErrNotFound + }) assert.Equal(t, http.StatusNotFound, request(e, "/sample")) diff --git a/go.mod b/go.mod index b557df9..84e24e6 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,16 @@ module github.com/labstack/echo-contrib -go 1.24.0 +go 1.25.0 require ( - github.com/casbin/casbin/v2 v2.134.0 + github.com/casbin/casbin/v2 v2.135.0 github.com/gorilla/context v1.1.2 github.com/gorilla/sessions v1.4.0 - github.com/labstack/echo/v4 v4.13.4 - github.com/labstack/gommon v0.4.2 + github.com/labstack/echo/v5 v5.0.0-20260118161441-9500f2745481 github.com/opentracing/opentracing-go v1.2.0 github.com/openzipkin/zipkin-go v0.4.3 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.4 + github.com/prometheus/common v0.67.5 github.com/stretchr/testify v1.11.1 github.com/uber/jaeger-client-go v2.30.0+incompatible ) @@ -19,30 +18,23 @@ require ( require ( github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.2 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d56a829..c19334d 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,10 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= -github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= -github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= +github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= +github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -48,14 +48,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= -github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/labstack/echo/v5 v5.0.0-20260118161441-9500f2745481 h1:tG+LG2uqpbTRjj71nq9a80XPol5NL9rk92lXCCysYdE= +github.com/labstack/echo/v5 v5.0.0-20260118161441-9500f2745481/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -71,8 +65,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -88,10 +82,6 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -100,8 +90,6 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -115,18 +103,17 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -140,10 +127,10 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/helpers/statuscode.go b/internal/helpers/statuscode.go new file mode 100644 index 0000000..9c03772 --- /dev/null +++ b/internal/helpers/statuscode.go @@ -0,0 +1,26 @@ +package helpers + +import ( + "errors" + "net/http" + + "github.com/labstack/echo/v5" +) + +// DefaultStatusResolver resolves http status code from given err or Response. +func DefaultStatusResolver(c *echo.Context, err error) int { + status := 0 + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + return sc.StatusCode() + } + if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if eResp.Committed { + status = eResp.Status + } + } + if err != nil && status == 0 { + status = http.StatusInternalServerError + } + return status +} diff --git a/jaegertracing/jaegertracing.go b/jaegertracing/jaegertracing.go index cf91e51..cd37663 100644 --- a/jaegertracing/jaegertracing.go +++ b/jaegertracing/jaegertracing.go @@ -10,7 +10,7 @@ package main import ( "github.com/labstack/echo-contrib/jaegertracing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) @@ -38,14 +38,15 @@ import ( "runtime" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo-contrib/internal/helpers" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/uber/jaeger-client-go/config" ) -const defaultComponentName = "echo/v4" +const defaultComponentName = "echo/v5" type ( // TraceConfig defines the config for Trace middleware. @@ -70,7 +71,7 @@ type ( LimitSize int // OperationNameFunc composes operation name based on context. Can be used to override default naming - OperationNameFunc func(c echo.Context) string + OperationNameFunc func(c *echo.Context) string } ) @@ -145,7 +146,7 @@ func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc { } return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } @@ -195,7 +196,7 @@ func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc { // response respDumper = newResponseDumper(c.Response()) - c.Response().Writer = respDumper + c.SetResponse(respDumper) } // setup request context - add opentracing span @@ -218,11 +219,8 @@ func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc { // call next middleware / controller err = next(c) - if err != nil { - c.Error(err) // call custom registered error handler - } - status := c.Response().Status + status := helpers.DefaultStatusResolver(c, err) ext.HTTPStatusCode.Set(sp, uint16(status)) if err != nil { @@ -261,7 +259,7 @@ func logError(span opentracing.Span, err error) { span.SetTag("error", true) } -func getRequestID(ctx echo.Context) string { +func getRequestID(ctx *echo.Context) string { requestID := ctx.Request().Header.Get(echo.HeaderXRequestID) // request-id generated by reverse-proxy if requestID == "" { requestID = generateToken() // missed request-id from proxy, we generate it manually @@ -275,13 +273,13 @@ func generateToken() string { return fmt.Sprintf("%x", b) } -func defaultOperationName(c echo.Context) string { +func defaultOperationName(c *echo.Context) string { req := c.Request() return "HTTP " + req.Method + " URL: " + c.Path() } // TraceFunction wraps funtion with opentracing span adding tags for the function name and caller details -func TraceFunction(ctx echo.Context, fn interface{}, params ...interface{}) (result []reflect.Value) { +func TraceFunction(ctx *echo.Context, fn interface{}, params ...interface{}) (result []reflect.Value) { // Get function name name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() // Create child span @@ -316,7 +314,7 @@ func TraceFunction(ctx echo.Context, fn interface{}, params ...interface{}) (res // CreateChildSpan creates a new opentracing span adding tags for the span name and caller details. // User must call defer `sp.Finish()` -func CreateChildSpan(ctx echo.Context, name string) opentracing.Span { +func CreateChildSpan(ctx *echo.Context, name string) opentracing.Span { parentSpan := opentracing.SpanFromContext(ctx.Request().Context()) sp := opentracing.StartSpan( name, diff --git a/jaegertracing/jaegertracing_test.go b/jaegertracing/jaegertracing_test.go index a06c62b..0815ac9 100644 --- a/jaegertracing/jaegertracing_test.go +++ b/jaegertracing/jaegertracing_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/log" "github.com/stretchr/testify/assert" @@ -139,15 +139,15 @@ func TestTraceWithDefaultConfig(t *testing.T) { e := echo.New() e.Use(Trace(tracer)) - e.GET("/hello", func(c echo.Context) error { + e.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "world") }) - e.GET("/giveme400", func(c echo.Context) error { + e.GET("/giveme400", func(c *echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "baaaad request") }) - e.GET("/givemeerror", func(c echo.Context) error { + e.GET("/givemeerror", func(c *echo.Context) error { return fmt.Errorf("internal stuff went wrong") }) @@ -224,7 +224,7 @@ func TestTraceWithConfigOfBodyDump(t *testing.T) { ComponentName: "EchoTracer", IsBodyDump: true, })) - e.POST("/trace", func(c echo.Context) error { + e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi") }) @@ -262,7 +262,7 @@ func TestTraceWithConfigOfSkip(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ - Skipper: func(echo.Context) bool { + Skipper: func(*echo.Context) bool { return true }, Tracer: tracer, @@ -296,7 +296,7 @@ func TestTraceWithLimitHTTPBody(t *testing.T) { LimitHTTPBody: true, LimitSize: 10, })) - e.POST("/trace", func(c echo.Context) error { + e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi 123456789012345678901234567890") }) @@ -320,7 +320,7 @@ func TestTraceWithoutLimitHTTPBody(t *testing.T) { LimitHTTPBody: false, // disabled LimitSize: 10, })) - e.POST("/trace", func(c echo.Context) error { + e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi 123456789012345678901234567890") }) @@ -339,7 +339,7 @@ func TestTraceWithDefaultOperationName(t *testing.T) { e := echo.New() e.Use(Trace(tracer)) - e.GET("/trace", func(c echo.Context) error { + e.GET("/trace", func(c *echo.Context) error { return c.String(http.StatusOK, "Hi") }) @@ -357,18 +357,16 @@ func TestTraceWithCustomOperationName(t *testing.T) { e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", - OperationNameFunc: func(c echo.Context) string { + OperationNameFunc: func(c *echo.Context) string { // This is an example of operation name customization // In most cases default formatting is more than enough req := c.Request() opName := "HTTP " + req.Method path := c.Path() - paramNames := c.ParamNames() - - for _, name := range paramNames { - from := ":" + name - to := "{" + name + "}" + for _, pv := range c.PathValues() { + from := ":" + pv.Name + to := "{" + pv.Name + "}" path = strings.ReplaceAll(path, from, to) } @@ -376,7 +374,7 @@ func TestTraceWithCustomOperationName(t *testing.T) { }, })) - e.GET("/trace/:traceID/spans/:spanID", func(c echo.Context) error { + e.GET("/trace/:traceID/spans/:spanID", func(c *echo.Context) error { return c.String(http.StatusOK, "Hi") }) diff --git a/jaegertracing/response_dumper.go b/jaegertracing/response_dumper.go index fdc1ac3..1dca44a 100644 --- a/jaegertracing/response_dumper.go +++ b/jaegertracing/response_dumper.go @@ -7,8 +7,6 @@ import ( "bytes" "io" "net/http" - - "github.com/labstack/echo/v4" ) type responseDumper struct { @@ -18,12 +16,12 @@ type responseDumper struct { buf *bytes.Buffer } -func newResponseDumper(resp *echo.Response) *responseDumper { +func newResponseDumper(resp http.ResponseWriter) *responseDumper { buf := new(bytes.Buffer) return &responseDumper{ - ResponseWriter: resp.Writer, + ResponseWriter: resp, - mw: io.MultiWriter(resp.Writer, buf), + mw: io.MultiWriter(resp, buf), buf: buf, } } @@ -35,3 +33,7 @@ func (d *responseDumper) Write(b []byte) (int, error) { func (d *responseDumper) GetResponse() string { return d.buf.String() } + +func (d *responseDumper) Unwrap() http.ResponseWriter { + return d.ResponseWriter +} diff --git a/pprof/pprof.go b/pprof/pprof.go index cbb2fd7..644e9c5 100644 --- a/pprof/pprof.go +++ b/pprof/pprof.go @@ -7,7 +7,7 @@ import ( "net/http" "net/http/pprof" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) const ( @@ -44,8 +44,8 @@ func Register(e *echo.Echo, prefixOptions ...string) { } func handler(h http.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - h.ServeHTTP(c.Response().Writer, c.Request()) + return func(c *echo.Context) error { + h.ServeHTTP(c.Response(), c.Request()) return nil } } diff --git a/pprof/pprof_test.go b/pprof/pprof_test.go index 5f1626a..a805716 100644 --- a/pprof/pprof_test.go +++ b/pprof/pprof_test.go @@ -9,7 +9,7 @@ import ( "net/http/httptest" "testing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func TestPProfRegisterDefaualtPrefix(t *testing.T) { diff --git a/prometheus/prometheus.go b/prometheus/prometheus.go deleted file mode 100644 index 27324a7..0000000 --- a/prometheus/prometheus.go +++ /dev/null @@ -1,489 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors - -/* -Package prometheus provides middleware to add Prometheus metrics. - -Example: -``` -package main -import ( - - "github.com/labstack/echo/v4" - "github.com/labstack/echo-contrib/prometheus" - -) - - func main() { - e := echo.New() - // Enable metrics middleware - p := prometheus.NewPrometheus("echo", nil) - p.Use(e) - - e.Logger.Fatal(e.Start(":1323")) - } - -``` -*/ -package prometheus - -import ( - "bytes" - "errors" - "net/http" - "os" - "strconv" - "time" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/log" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/prometheus/common/expfmt" -) - -var defaultMetricPath = "/metrics" -var defaultSubsystem = "echo" - -const ( - _ = iota // ignore first value by assigning to blank identifier - KB float64 = 1 << (10 * iota) - MB - GB - TB -) - -// reqDurBuckets is the buckets for request duration. Here, we use the prometheus defaults -// which are for ~10s request length max: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} -var reqDurBuckets = prometheus.DefBuckets - -// reqSzBuckets is the buckets for request size. Here we define a spectrom from 1KB thru 1NB up to 10MB. -var reqSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} - -// resSzBuckets is the buckets for response size. Here we define a spectrom from 1KB thru 1NB up to 10MB. -var resSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} - -// Standard default metrics -// -// counter, counter_vec, gauge, gauge_vec, -// histogram, histogram_vec, summary, summary_vec -var reqCnt = &Metric{ - ID: "reqCnt", - Name: "requests_total", - Description: "How many HTTP requests processed, partitioned by status code and HTTP method.", - Type: "counter_vec", - Args: []string{"code", "method", "host", "url"}} - -var reqDur = &Metric{ - ID: "reqDur", - Name: "request_duration_seconds", - Description: "The HTTP request latencies in seconds.", - Args: []string{"code", "method", "host", "url"}, - Type: "histogram_vec", - Buckets: reqDurBuckets} - -var resSz = &Metric{ - ID: "resSz", - Name: "response_size_bytes", - Description: "The HTTP response sizes in bytes.", - Args: []string{"code", "method", "host", "url"}, - Type: "histogram_vec", - Buckets: resSzBuckets} - -var reqSz = &Metric{ - ID: "reqSz", - Name: "request_size_bytes", - Description: "The HTTP request sizes in bytes.", - Args: []string{"code", "method", "host", "url"}, - Type: "histogram_vec", - Buckets: reqSzBuckets} - -var standardMetrics = []*Metric{ - reqCnt, - reqDur, - resSz, - reqSz, -} - -/* -RequestCounterLabelMappingFunc is a function which can be supplied to the middleware to control -the cardinality of the request counter's "url" label, which might be required in some contexts. -For instance, if for a "/customer/:name" route you don't want to generate a time series for every -possible customer name, you could use this function: - - func(c echo.Context) string { - url := c.Request.URL.Path - for _, p := range c.Params { - if p.Key == "name" { - url = strings.Replace(url, p.Value, ":name", 1) - break - } - } - return url - } - -which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". -It can also be applied for the "Host" label -*/ -type RequestCounterLabelMappingFunc func(c echo.Context) string - -// Metric is a definition for the name, description, type, ID, and -// prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric -type Metric struct { - MetricCollector prometheus.Collector - ID string - Name string - Description string - Type string - Args []string - Buckets []float64 -} - -// Prometheus contains the metrics gathered by the instance and its path -// Deprecated: use echoprometheus package instead -type Prometheus struct { - reqCnt *prometheus.CounterVec - reqDur, reqSz, resSz *prometheus.HistogramVec - router *echo.Echo - listenAddress string - Ppg PushGateway - - MetricsList []*Metric - MetricsPath string - Subsystem string - Skipper middleware.Skipper - - RequestCounterURLLabelMappingFunc RequestCounterLabelMappingFunc - RequestCounterHostLabelMappingFunc RequestCounterLabelMappingFunc - - // Context string to use as a prometheus URL label - URLLabelFromContext string -} - -// PushGateway contains the configuration for pushing to a Prometheus pushgateway (optional) -type PushGateway struct { - // Push interval in seconds - //lint:ignore ST1011 renaming would be breaking change - PushIntervalSeconds time.Duration - - // Push Gateway URL in format http://domain:port - // where JOBNAME can be any string of your choice - PushGatewayURL string - - // pushgateway job name, defaults to "echo" - Job string -} - -// NewPrometheus generates a new set of metrics with a certain subsystem name -// Deprecated: use echoprometheus package instead -func NewPrometheus(subsystem string, skipper middleware.Skipper, customMetricsList ...[]*Metric) *Prometheus { - var metricsList []*Metric - if skipper == nil { - skipper = middleware.DefaultSkipper - } - - if len(customMetricsList) > 1 { - panic("Too many args. NewPrometheus( string, ).") - } else if len(customMetricsList) == 1 { - metricsList = customMetricsList[0] - } - - metricsList = append(metricsList, standardMetrics...) - - p := &Prometheus{ - MetricsList: metricsList, - MetricsPath: defaultMetricPath, - Subsystem: defaultSubsystem, - Skipper: skipper, - RequestCounterURLLabelMappingFunc: func(c echo.Context) string { - p := c.Path() // contains route path ala `/users/:id` - if p != "" { - return p - } - // as of Echo v4.10.1 path is empty for 404 cases (when router did not find any matching routes) - // in this case we use actual path from request to have some distinction in Prometheus - return c.Request().URL.Path - }, - RequestCounterHostLabelMappingFunc: func(c echo.Context) string { - return c.Request().Host - }, - } - - p.registerMetrics(subsystem) - - return p -} - -// SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL -// every pushInterval. Metrics are fetched from -func (p *Prometheus) SetPushGateway(pushGatewayURL string, pushInterval time.Duration) { - p.Ppg.PushGatewayURL = pushGatewayURL - p.Ppg.PushIntervalSeconds = pushInterval - p.startPushTicker() -} - -// SetPushGatewayJob job name, defaults to "echo" -func (p *Prometheus) SetPushGatewayJob(j string) { - p.Ppg.Job = j -} - -// SetListenAddress for exposing metrics on address. If not set, it will be exposed at the -// same address of the echo engine that is being used -// func (p *Prometheus) SetListenAddress(address string) { -// p.listenAddress = address -// if p.listenAddress != "" { -// p.router = echo.Echo().Router() -// } -// } - -// SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of -// your content's access log). -// func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *echo.Echo) { -// p.listenAddress = listenAddress -// if len(p.listenAddress) > 0 { -// p.router = r -// } -// } - -// SetMetricsPath set metrics paths -func (p *Prometheus) SetMetricsPath(e *echo.Echo) { - if p.listenAddress != "" { - p.router.GET(p.MetricsPath, prometheusHandler()) - p.runServer() - } else { - e.GET(p.MetricsPath, prometheusHandler()) - } -} - -func (p *Prometheus) runServer() { - if p.listenAddress != "" { - go p.router.Start(p.listenAddress) - } -} - -func (p *Prometheus) getMetrics() []byte { - out := &bytes.Buffer{} - metricFamilies, _ := prometheus.DefaultGatherer.Gather() - for i := range metricFamilies { - expfmt.MetricFamilyToText(out, metricFamilies[i]) - - } - return out.Bytes() -} - -func (p *Prometheus) getPushGatewayURL() string { - h, _ := os.Hostname() - if p.Ppg.Job == "" { - p.Ppg.Job = "echo" - } - return p.Ppg.PushGatewayURL + "/metrics/job/" + p.Ppg.Job + "/instance/" + h -} - -func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) { - req, err := http.NewRequest("POST", p.getPushGatewayURL(), bytes.NewBuffer(metrics)) - if err != nil { - log.Errorf("failed to create push gateway request: %v", err) - return - } - client := &http.Client{} - if _, err = client.Do(req); err != nil { - log.Errorf("Error sending to push gateway: %v", err) - } -} - -func (p *Prometheus) startPushTicker() { - ticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds) - go func() { - for range ticker.C { - p.sendMetricsToPushGateway(p.getMetrics()) - } - }() -} - -// NewMetric associates prometheus.Collector based on Metric.Type -// Deprecated: use echoprometheus package instead -func NewMetric(m *Metric, subsystem string) prometheus.Collector { - var metric prometheus.Collector - switch m.Type { - case "counter_vec": - metric = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - m.Args, - ) - case "counter": - metric = prometheus.NewCounter( - prometheus.CounterOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - ) - case "gauge_vec": - metric = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - m.Args, - ) - case "gauge": - metric = prometheus.NewGauge( - prometheus.GaugeOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - ) - case "histogram_vec": - metric = prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - Buckets: m.Buckets, - }, - m.Args, - ) - case "histogram": - metric = prometheus.NewHistogram( - prometheus.HistogramOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - Buckets: m.Buckets, - }, - ) - case "summary_vec": - metric = prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - m.Args, - ) - case "summary": - metric = prometheus.NewSummary( - prometheus.SummaryOpts{ - Subsystem: subsystem, - Name: m.Name, - Help: m.Description, - }, - ) - } - return metric -} - -func (p *Prometheus) registerMetrics(subsystem string) { - - for _, metricDef := range p.MetricsList { - metric := NewMetric(metricDef, subsystem) - if err := prometheus.Register(metric); err != nil { - log.Errorf("%s could not be registered in Prometheus: %v", metricDef.Name, err) - } - switch metricDef { - case reqCnt: - p.reqCnt = metric.(*prometheus.CounterVec) - case reqDur: - p.reqDur = metric.(*prometheus.HistogramVec) - case resSz: - p.resSz = metric.(*prometheus.HistogramVec) - case reqSz: - p.reqSz = metric.(*prometheus.HistogramVec) - } - metricDef.MetricCollector = metric - } -} - -// Use adds the middleware to the Echo engine. -func (p *Prometheus) Use(e *echo.Echo) { - e.Use(p.HandlerFunc) - p.SetMetricsPath(e) -} - -// HandlerFunc defines handler function for middleware -func (p *Prometheus) HandlerFunc(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if c.Path() == p.MetricsPath { - return next(c) - } - if p.Skipper(c) { - return next(c) - } - - start := time.Now() - reqSz := computeApproximateRequestSize(c.Request()) - - err := next(c) - - status := c.Response().Status - if err != nil { - var httpError *echo.HTTPError - if errors.As(err, &httpError) { - status = httpError.Code - } - if status == 0 || status == http.StatusOK { - status = http.StatusInternalServerError - } - } - - elapsed := float64(time.Since(start)) / float64(time.Second) - - url := p.RequestCounterURLLabelMappingFunc(c) - if len(p.URLLabelFromContext) > 0 { - u := c.Get(p.URLLabelFromContext) - if u == nil { - u = "unknown" - } - url = u.(string) - } - - statusStr := strconv.Itoa(status) - p.reqDur.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(elapsed) - p.reqCnt.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Inc() - p.reqSz.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(float64(reqSz)) - - resSz := float64(c.Response().Size) - p.resSz.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(resSz) - - return err - } -} - -func prometheusHandler() echo.HandlerFunc { - h := promhttp.Handler() - return func(c echo.Context) error { - h.ServeHTTP(c.Response(), c.Request()) - return nil - } -} - -func computeApproximateRequestSize(r *http.Request) int { - s := 0 - if r.URL != nil { - s = len(r.URL.Path) - } - - s += len(r.Method) - s += len(r.Proto) - for name, values := range r.Header { - s += len(name) - for _, value := range values { - s += len(value) - } - } - s += len(r.Host) - - // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. - - if r.ContentLength != -1 { - s += int(r.ContentLength) - } - return s -} diff --git a/prometheus/prometheus_test.go b/prometheus/prometheus_test.go deleted file mode 100644 index 8c4f40a..0000000 --- a/prometheus/prometheus_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors - -package prometheus - -import ( - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/labstack/echo/v4" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" -) - -func unregister(p *Prometheus) { - prometheus.Unregister(p.reqCnt) - prometheus.Unregister(p.reqDur) - prometheus.Unregister(p.reqSz) - prometheus.Unregister(p.resSz) -} - -func TestPrometheus_Use(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - assert.Equal(t, 1, len(e.Routes()), "only one route should be added") - assert.NotNil(t, e, "the engine should not be empty") - assert.Equal(t, e.Routes()[0].Path, p.MetricsPath, "the path should match the metrics path") - unregister(p) -} - -func TestPrometheus_Buckets(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - path := "/ping" - - req := httptest.NewRequest(http.MethodGet, path, nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNotFound, rec.Code) - - req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - assert.Contains(t, rec.Body.String(), fmt.Sprintf("%s_request_duration_seconds", p.Subsystem)) - - body := rec.Body.String() - assert.Contains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "duration should have time bucket (like, 0.005s)") - assert.NotContains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="512000"}`, "duration should NOT have a size bucket (like, 512K)") - assert.Contains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "request size should have a 1024k (size) bucket") - assert.NotContains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "request size should NOT have time bucket (like, 0.005s)") - assert.Contains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "response size should have a 1024k (size) bucket") - assert.NotContains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "response size should NOT have time bucket (like, 0.005s)") - - unregister(p) -} - -func TestPath(t *testing.T) { - p := NewPrometheus("echo", nil) - assert.Equal(t, p.MetricsPath, defaultMetricPath, "no usage of path should yield default path") - unregister(p) -} - -func TestSubsystem(t *testing.T) { - p := NewPrometheus("echo", nil) - assert.Equal(t, p.Subsystem, "echo", "subsystem should be default") - unregister(p) -} - -func TestUse(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - - req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNotFound, rec.Code) - - p.Use(e) - - req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - unregister(p) -} - -func TestIgnore(t *testing.T) { - e := echo.New() - - ipath := "/ping" - lipath := fmt.Sprintf(`path="%s"`, ipath) - ignore := func(c echo.Context) bool { - if strings.HasPrefix(c.Path(), ipath) { - return true - } - return false - } - p := NewPrometheus("echo", ignore) - p.Use(e) - - req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_requests_total", p.Subsystem)) - - req = httptest.NewRequest(http.MethodGet, "/ping", nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNotFound, rec.Code) - - req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - assert.NotContains(t, rec.Body.String(), lipath, "ignored path must not be present") - - unregister(p) -} - -func TestMetricsGenerated(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - req := httptest.NewRequest(http.MethodGet, "/ping?test=1", nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNotFound, rec.Code) - - req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - s := rec.Body.String() - assert.Contains(t, s, `url="/ping"`, "path must be present") - assert.Contains(t, s, `host="example.com"`, "host must be present") - - unregister(p) -} - -func TestMetricsPathIgnored(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_requests_total", p.Subsystem)) - unregister(p) -} - -func TestMetricsPushGateway(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_request_duration", p.Subsystem)) - - unregister(p) -} - -func TestMetricsForErrors(t *testing.T) { - e := echo.New() - p := NewPrometheus("echo", nil) - p.Use(e) - - e.GET("/handler_for_ok", func(c echo.Context) error { - return c.JSON(http.StatusOK, "OK") - }) - e.GET("/handler_for_nok", func(c echo.Context) error { - return c.JSON(http.StatusConflict, "NOK") - }) - e.GET("/handler_for_error", func(c echo.Context) error { - return echo.NewHTTPError(http.StatusBadGateway, "BAD") - }) - - req := httptest.NewRequest(http.MethodGet, "/handler_for_ok", nil) - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - req = httptest.NewRequest(http.MethodGet, "/handler_for_nok", nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusConflict, rec.Code) - - req = httptest.NewRequest(http.MethodGet, "/handler_for_nok", nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusConflict, rec.Code) - - req = httptest.NewRequest(http.MethodGet, "/handler_for_error", nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadGateway, rec.Code) - - req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) - rec = httptest.NewRecorder() - e.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - body := rec.Body.String() - assert.Contains(t, body, fmt.Sprintf("%s_requests_total", p.Subsystem)) - assert.Contains(t, body, `echo_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) - assert.Contains(t, body, `echo_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 2`) - assert.Contains(t, body, `echo_requests_total{code="502",host="example.com",method="GET",url="/handler_for_error"} 1`) - - unregister(p) -} diff --git a/session/session.go b/session/session.go index e4386a2..3e30864 100644 --- a/session/session.go +++ b/session/session.go @@ -8,8 +8,8 @@ import ( "github.com/gorilla/context" "github.com/gorilla/sessions" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) type ( @@ -36,7 +36,7 @@ var ( ) // Get returns a named session. -func Get(name string, c echo.Context) (*sessions.Session, error) { +func Get(name string, c *echo.Context) (*sessions.Session, error) { s := c.Get(key) if s == nil { return nil, fmt.Errorf("%q session store not found", key) @@ -64,7 +64,7 @@ func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { } return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } diff --git a/session/session_test.go b/session/session_test.go index 552245c..2d71091 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -10,16 +10,16 @@ import ( "testing" "github.com/gorilla/sessions" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) func TestMiddleware(t *testing.T) { e := echo.New() - req := httptest.NewRequest(echo.GET, "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { sess, _ := Get("test", c) sess.Options.Domain = "labstack.com" sess.Values["foo"] = "bar" @@ -30,7 +30,7 @@ func TestMiddleware(t *testing.T) { } store := sessions.NewCookieStore([]byte("secret")) config := Config{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return true }, Store: store, @@ -38,7 +38,9 @@ func TestMiddleware(t *testing.T) { // Skipper mw := MiddlewareWithConfig(config) - h := mw(echo.NotFoundHandler) + h := mw(func(c *echo.Context) error { + return echo.ErrNotFound + }) assert.Error(t, h(c)) // 404 assert.Nil(t, c.Get(key)) @@ -59,7 +61,7 @@ func TestMiddleware(t *testing.T) { func TestGetSessionMissingStore(t *testing.T) { e := echo.New() - req := httptest.NewRequest(echo.GET, "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _, err := Get("test", c) diff --git a/zipkintracing/response_writer.go b/zipkintracing/response_writer.go index 407b193..1c557a1 100644 --- a/zipkintracing/response_writer.go +++ b/zipkintracing/response_writer.go @@ -107,3 +107,7 @@ func (rw *responseWriter) CloseNotify() <-chan bool { //lint:ignore SA1019 we support it for backwards compatibility reasons return rw.ResponseWriter.(http.CloseNotifier).CloseNotify() } + +func (rw *responseWriter) Unwrap() http.ResponseWriter { + return rw.ResponseWriter +} diff --git a/zipkintracing/tracing.go b/zipkintracing/tracing.go index ae0fca4..97fd38d 100644 --- a/zipkintracing/tracing.go +++ b/zipkintracing/tracing.go @@ -5,11 +5,12 @@ package zipkintracing import ( "fmt" - "github.com/labstack/echo/v4/middleware" "net/http" "strconv" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5/middleware" + + "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" "github.com/openzipkin/zipkin-go/model" @@ -19,7 +20,7 @@ import ( type ( //Tags func to adds span tags - Tags func(c echo.Context) map[string]string + Tags func(c *echo.Context) map[string]string //TraceProxyConfig config for TraceProxyWithConfig TraceProxyConfig struct { @@ -38,7 +39,7 @@ type ( var ( //DefaultSpanTags default span tags - DefaultSpanTags = func(c echo.Context) map[string]string { + DefaultSpanTags = func(c *echo.Context) map[string]string { return make(map[string]string) } @@ -50,13 +51,13 @@ var ( ) // DoHTTP is a http zipkin tracer implementation of HTTPDoer -func DoHTTP(c echo.Context, r *http.Request, client *zipkinhttp.Client) (*http.Response, error) { +func DoHTTP(c *echo.Context, r *http.Request, client *zipkinhttp.Client) (*http.Response, error) { req := r.WithContext(c.Request().Context()) return client.DoWithAppSpan(req, req.Method) } // TraceFunc wraps function call with span so that we can trace time taken by func, eventContext only provided if we want to store trace headers -func TraceFunc(c echo.Context, spanName string, spanTags Tags, tracer *zipkin.Tracer) func() { +func TraceFunc(c *echo.Context, spanName string, spanTags Tags, tracer *zipkin.Tracer) func() { span, _ := tracer.StartSpanFromContext(c.Request().Context(), spanName) for key, value := range spanTags(c) { span.Tag(key, value) @@ -79,7 +80,7 @@ func TraceProxy(tracer *zipkin.Tracer) echo.MiddlewareFunc { // TraceProxyWithConfig middleware that traces reverse proxy func TraceProxyWithConfig(config TraceProxyConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } @@ -95,9 +96,9 @@ func TraceProxyWithConfig(config TraceProxyConfig) echo.MiddlewareFunc { ctx := zipkin.NewContext(c.Request().Context(), span) c.SetRequest(c.Request().WithContext(ctx)) b3.InjectHTTP(c.Request())(span.Context()) - nrw := NewResponseWriter(c.Response().Writer) + nrw := NewResponseWriter(c.Response()) if err := next(c); err != nil { - c.Error(err) + c.Echo().HTTPErrorHandler(c, err) } if nrw.Size() > 0 { zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(int64(nrw.Size()), 10)) @@ -124,7 +125,7 @@ func TraceServer(tracer *zipkin.Tracer) echo.MiddlewareFunc { // TraceServerWithConfig middleware that traces server calls func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } @@ -136,9 +137,9 @@ func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc { defer span.Finish() ctx := zipkin.NewContext(c.Request().Context(), span) c.SetRequest(c.Request().WithContext(ctx)) - nrw := NewResponseWriter(c.Response().Writer) + nrw := NewResponseWriter(c.Response()) if err := next(c); err != nil { - c.Error(err) + c.Echo().HTTPErrorHandler(c, err) } if nrw.Size() > 0 { @@ -158,7 +159,7 @@ func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc { // StartChildSpan starts a new child span as child of parent span from context // user must call defer childSpan.Finish() -func StartChildSpan(c echo.Context, spanName string, tracer *zipkin.Tracer) (childSpan zipkin.Span) { +func StartChildSpan(c *echo.Context, spanName string, tracer *zipkin.Tracer) (childSpan zipkin.Span) { var parentContext model.SpanContext if span := zipkin.SpanFromContext(c.Request().Context()); span != nil { diff --git a/zipkintracing/tracing_test.go b/zipkintracing/tracing_test.go index 7f4e2e2..3c7ac72 100644 --- a/zipkintracing/tracing_test.go +++ b/zipkintracing/tracing_test.go @@ -5,8 +5,8 @@ package zipkintracing import ( "encoding/json" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" "github.com/openzipkin/zipkin-go/propagation/b3" @@ -161,7 +161,7 @@ func TestTraceProxy(t *testing.T) { e := echo.New() c := e.NewContext(req, rec) mw := TraceProxy(tracer) - h := mw(func(c echo.Context) error { + h := mw(func(c *echo.Context) error { return nil }) err = h(c) @@ -205,7 +205,7 @@ func TestTraceServer(t *testing.T) { req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) rec := httptest.NewRecorder() mw := TraceServer(tracer) - h := mw(func(c echo.Context) error { + h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err) @@ -251,7 +251,7 @@ func TestTraceServerWithConfig(t *testing.T) { req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) req.Header.Add("Client-Correlation-Id", "c98404736319") rec := httptest.NewRecorder() - tags := func(c echo.Context) map[string]string { + tags := func(c *echo.Context) map[string]string { tags := make(map[string]string) correlationID := c.Request().Header.Get("Client-Correlation-Id") tags["Client-Correlation-Id"] = correlationID @@ -259,7 +259,7 @@ func TestTraceServerWithConfig(t *testing.T) { } config := TraceServerConfig{Skipper: middleware.DefaultSkipper, SpanTags: tags, Tracer: tracer} mw := TraceServerWithConfig(config) - h := mw(func(c echo.Context) error { + h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err) @@ -292,11 +292,11 @@ func TestTraceServerWithConfigSkipper(t *testing.T) { traceTags["availability_zone"] = "us-east-1" req := httptest.NewRequest("GET", "http://localhost:8080/health", nil) rec := httptest.NewRecorder() - config := TraceServerConfig{Skipper: func(c echo.Context) bool { + config := TraceServerConfig{Skipper: func(c *echo.Context) bool { return c.Request().URL.Path == "/health" }, Tracer: tracer} mw := TraceServerWithConfig(config) - h := mw(func(c echo.Context) error { + h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err)