diff --git a/docs/api/middleware/healthcheck.md b/docs/api/middleware/healthcheck.md index 666d51ef12..4c246046a1 100644 --- a/docs/api/middleware/healthcheck.md +++ b/docs/api/middleware/healthcheck.md @@ -39,19 +39,39 @@ import ( After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can use the following possibilities: ```go -// Provide a minimal config -app.Use(healthcheck.New()) +// Provide a minimal config for liveness check +app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.New()) +// Provide a minimal config for readiness check +app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.New()) +// Provide a minimal config for check with custom endpoint +app.Get("/live", healthcheck.New()) // Or extend your config for customization -app.Use(healthcheck.New(healthcheck.Config{ - LivenessProbe: func(c fiber.Ctx) bool { +app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.New(healthcheck.Config{ + Probe: func(c fiber.Ctx) bool { return true }, - LivenessEndpoint: "/live", - ReadinessProbe: func(c fiber.Ctx) bool { - return serviceA.Ready() && serviceB.Ready() && ... +})) +// And it works the same for readiness, just change the route +app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.New(healthcheck.Config{ + Probe: func(c fiber.Ctx) bool { + return true + }, +})) +// With a custom route and custom probe +app.Get("/live", healthcheck.New(healthcheck.Config{ + Probe: func(c fiber.Ctx) bool { + return true + }, +})) + +// It can also be used with app.All, although it will only respond to requests with the GET method +// in case of calling the route with any method which isn't GET, the return will be 404 Not Found when app.All is used +// and 405 Method Not Allowed when app.Get is used +app.All(healthcheck.DefaultReadinessEndpoint, healthcheck.New(healthcheck.Config{ + Probe: func(c fiber.Ctx) bool { + return true }, - ReadinessEndpoint: "/ready", })) ``` @@ -59,7 +79,9 @@ app.Use(healthcheck.New(healthcheck.Config{ ```go type Config struct { - // Next defines a function to skip this middleware when returned true. + // Next defines a function to skip this middleware when returned true. If this function returns true + // and no other handlers are defined for the route, Fiber will return a status 404 Not Found, since + // no other handlers were defined to return a different status. // // Optional. Default: nil Next func(fiber.Ctx) bool @@ -69,23 +91,7 @@ type Config struct { // the application is in a state where it can handle requests (e.g., the server is up and running). // // Optional. Default: func(c fiber.Ctx) bool { return true } - LivenessProbe HealthChecker - - // HTTP endpoint at which the liveness probe will be available. - // - // Optional. Default: "/livez" - LivenessEndpoint string - - // Function used for checking the readiness of the application. Returns true if the application - // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary - // services, databases, and other dependencies are available for the application to function correctly. - // - // Optional. Default: func(c fiber.Ctx) bool { return true } - ReadinessProbe HealthChecker - - // HTTP endpoint at which the readiness probe will be available. - // Optional. Default: "/readyz" - ReadinessEndpoint string + Probe HealthChecker } ``` @@ -93,14 +99,9 @@ type Config struct { The default configuration used by this middleware is defined as follows: ```go -func defaultLivenessProbe(fiber.Ctx) bool { return true } - -func defaultReadinessProbe(fiber.Ctx) bool { return true } +func defaultProbe(fiber.Ctx) bool { return true } var ConfigDefault = Config{ - LivenessProbe: defaultLivenessProbe, - ReadinessProbe: defaultReadinessProbe, - LivenessEndpoint: "/livez", - ReadinessEndpoint: "/readyz", + Probe: defaultProbe, } ``` diff --git a/middleware/healthcheck/config.go b/middleware/healthcheck/config.go index 59916fc869..8b7b26a267 100644 --- a/middleware/healthcheck/config.go +++ b/middleware/healthcheck/config.go @@ -6,7 +6,9 @@ import ( // Config defines the configuration options for the healthcheck middleware. type Config struct { - // Next defines a function to skip this middleware when returned true. + // Next defines a function to skip this middleware when returned true. If this function returns true + // and no other handlers are defined for the route, Fiber will return a status 404 Not Found, since + // no other handlers were defined to return a different status. // // Optional. Default: nil Next func(fiber.Ctx) bool @@ -16,23 +18,7 @@ type Config struct { // the application is in a state where it can handle requests (e.g., the server is up and running). // // Optional. Default: func(c fiber.Ctx) bool { return true } - LivenessProbe HealthChecker - - // HTTP endpoint at which the liveness probe will be available. - // - // Optional. Default: "/livez" - LivenessEndpoint string - - // Function used for checking the readiness of the application. Returns true if the application - // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary - // services, databases, and other dependencies are available for the application to function correctly. - // - // Optional. Default: func(c fiber.Ctx) bool { return true } - ReadinessProbe HealthChecker - - // HTTP endpoint at which the readiness probe will be available. - // Optional. Default: "/readyz" - ReadinessEndpoint string + Probe HealthChecker } const ( @@ -40,44 +26,19 @@ const ( DefaultReadinessEndpoint = "/readyz" ) -func defaultLivenessProbe(fiber.Ctx) bool { return true } - -func defaultReadinessProbe(fiber.Ctx) bool { return true } - -// ConfigDefault is the default config -var ConfigDefault = Config{ - LivenessProbe: defaultLivenessProbe, - ReadinessProbe: defaultReadinessProbe, - LivenessEndpoint: DefaultLivenessEndpoint, - ReadinessEndpoint: DefaultReadinessEndpoint, -} +func defaultProbe(fiber.Ctx) bool { return true } -// defaultConfig returns a default config for the healthcheck middleware. -func defaultConfig(config ...Config) Config { +func defaultConfigV3(config ...Config) Config { if len(config) < 1 { - return ConfigDefault + return Config{ + Probe: defaultProbe, + } } cfg := config[0] - if cfg.Next == nil { - cfg.Next = ConfigDefault.Next - } - - if cfg.LivenessProbe == nil { - cfg.LivenessProbe = defaultLivenessProbe - } - - if cfg.ReadinessProbe == nil { - cfg.ReadinessProbe = defaultReadinessProbe - } - - if cfg.LivenessEndpoint == "" { - cfg.LivenessEndpoint = DefaultLivenessEndpoint - } - - if cfg.ReadinessEndpoint == "" { - cfg.ReadinessEndpoint = DefaultReadinessEndpoint + if cfg.Probe == nil { + cfg.Probe = defaultProbe } return cfg diff --git a/middleware/healthcheck/healthcheck.go b/middleware/healthcheck/healthcheck.go index 222b6f4452..51a16d70d3 100644 --- a/middleware/healthcheck/healthcheck.go +++ b/middleware/healthcheck/healthcheck.go @@ -1,36 +1,14 @@ package healthcheck import ( - "strings" - "github.com/gofiber/fiber/v3" ) // HealthChecker defines a function to check liveness or readiness of the application type HealthChecker func(fiber.Ctx) bool -// HealthCheckerHandler defines a function that returns a HealthChecker -type HealthCheckerHandler func(HealthChecker) fiber.Handler - -func healthCheckerHandler(checker HealthChecker) fiber.Handler { - return func(c fiber.Ctx) error { - if checker == nil { - return c.Next() - } - - if checker(c) { - return c.SendStatus(fiber.StatusOK) - } - - return c.SendStatus(fiber.StatusServiceUnavailable) - } -} - -func New(config ...Config) fiber.Handler { - cfg := defaultConfig(config...) - - isLiveHandler := healthCheckerHandler(cfg.LivenessProbe) - isReadyHandler := healthCheckerHandler(cfg.ReadinessProbe) +func NewHealthChecker(config ...Config) fiber.Handler { + cfg := defaultConfigV3(config...) return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true @@ -42,21 +20,10 @@ func New(config ...Config) fiber.Handler { return c.Next() } - prefixCount := len(strings.TrimRight(c.Route().Path, "/")) - if len(c.Path()) >= prefixCount { - checkPath := c.Path()[prefixCount:] - checkPathTrimmed := checkPath - if !c.App().Config().StrictRouting { - checkPathTrimmed = strings.TrimRight(checkPath, "/") - } - switch { - case checkPath == cfg.ReadinessEndpoint || checkPathTrimmed == cfg.ReadinessEndpoint: - return isReadyHandler(c) - case checkPath == cfg.LivenessEndpoint || checkPathTrimmed == cfg.LivenessEndpoint: - return isLiveHandler(c) - } + if cfg.Probe(c) { + return c.SendStatus(fiber.StatusOK) } - return c.Next() + return c.SendStatus(fiber.StatusServiceUnavailable) } } diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go index 409b985a60..e6c1748729 100644 --- a/middleware/healthcheck/healthcheck_test.go +++ b/middleware/healthcheck/healthcheck_test.go @@ -34,7 +34,8 @@ func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { StrictRouting: true, }) - app.Use(New()) + app.Get("/livez", NewHealthChecker()) + app.Get("/readyz", NewHealthChecker()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") @@ -44,71 +45,12 @@ func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { shouldGiveNotFound(t, app, "/notDefined/livez") } -func Test_HealthCheck_Group_Default(t *testing.T) { - t.Parallel() - - app := fiber.New() - app.Group("/v1", New()) - v2Group := app.Group("/v2/") - customer := v2Group.Group("/customer/") - customer.Use(New()) - - v3Group := app.Group("/v3/") - v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) - - // Testing health check endpoints in versioned API groups - shouldGiveOK(t, app, "/v1/readyz") - shouldGiveOK(t, app, "/v1/livez") - shouldGiveOK(t, app, "/v1/readyz/") - shouldGiveOK(t, app, "/v1/livez/") - shouldGiveOK(t, app, "/v2/customer/readyz") - shouldGiveOK(t, app, "/v2/customer/livez") - shouldGiveOK(t, app, "/v2/customer/readyz/") - shouldGiveOK(t, app, "/v2/customer/livez/") - shouldGiveNotFound(t, app, "/v3/todos/readyz") - shouldGiveNotFound(t, app, "/v3/todos/livez") - shouldGiveOK(t, app, "/v3/todos/readyz/") - shouldGiveOK(t, app, "/v3/todos/livez/") - shouldGiveNotFound(t, app, "/notDefined/readyz") - shouldGiveNotFound(t, app, "/notDefined/livez") - shouldGiveNotFound(t, app, "/notDefined/readyz/") - shouldGiveNotFound(t, app, "/notDefined/livez/") - - // strict routing - app = fiber.New(fiber.Config{ - StrictRouting: true, - }) - app.Group("/v1", New()) - v2Group = app.Group("/v2/") - customer = v2Group.Group("/customer/") - customer.Use(New()) - - v3Group = app.Group("/v3/") - v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) - - shouldGiveOK(t, app, "/v1/readyz") - shouldGiveOK(t, app, "/v1/livez") - shouldGiveNotFound(t, app, "/v1/readyz/") - shouldGiveNotFound(t, app, "/v1/livez/") - shouldGiveOK(t, app, "/v2/customer/readyz") - shouldGiveOK(t, app, "/v2/customer/livez") - shouldGiveNotFound(t, app, "/v2/customer/readyz/") - shouldGiveNotFound(t, app, "/v2/customer/livez/") - shouldGiveNotFound(t, app, "/v3/todos/readyz") - shouldGiveNotFound(t, app, "/v3/todos/livez") - shouldGiveOK(t, app, "/v3/todos/readyz/") - shouldGiveOK(t, app, "/v3/todos/livez/") - shouldGiveNotFound(t, app, "/notDefined/readyz") - shouldGiveNotFound(t, app, "/notDefined/livez") - shouldGiveNotFound(t, app, "/notDefined/readyz/") - shouldGiveNotFound(t, app, "/notDefined/livez/") -} - func Test_HealthCheck_Default(t *testing.T) { t.Parallel() app := fiber.New() - app.Use(New()) + app.Get("/livez", NewHealthChecker()) + app.Get("/readyz", NewHealthChecker()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") @@ -122,14 +64,14 @@ func Test_HealthCheck_Custom(t *testing.T) { t.Parallel() app := fiber.New() - c1 := make(chan struct{}, 1) - app.Use(New(Config{ - LivenessProbe: func(_ fiber.Ctx) bool { + app.Get("/live", NewHealthChecker(Config{ + Probe: func(_ fiber.Ctx) bool { return true }, - LivenessEndpoint: "/live", - ReadinessProbe: func(_ fiber.Ctx) bool { + })) + app.Get("/ready", NewHealthChecker(Config{ + Probe: func(_ fiber.Ctx) bool { select { case <-c1: return true @@ -137,7 +79,6 @@ func Test_HealthCheck_Custom(t *testing.T) { return false } }, - ReadinessEndpoint: "/ready", })) // Setup custom liveness and readiness probes to simulate application health status @@ -146,12 +87,12 @@ func Test_HealthCheck_Custom(t *testing.T) { // Live should return 404 with POST request req, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil)) require.NoError(t, err) - require.Equal(t, fiber.StatusNotFound, req.StatusCode) + require.Equal(t, fiber.StatusMethodNotAllowed, req.StatusCode) // Ready should return 404 with POST request req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/ready", nil)) require.NoError(t, err) - require.Equal(t, fiber.StatusNotFound, req.StatusCode) + require.Equal(t, fiber.StatusMethodNotAllowed, req.StatusCode) // Ready should return 503 with GET request before the channel is closed shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable) @@ -167,13 +108,13 @@ func Test_HealthCheck_Custom_Nested(t *testing.T) { app := fiber.New() c1 := make(chan struct{}, 1) - - app.Use(New(Config{ - LivenessProbe: func(_ fiber.Ctx) bool { + app.Get("/probe/live", NewHealthChecker(Config{ + Probe: func(_ fiber.Ctx) bool { return true }, - LivenessEndpoint: "/probe/live", - ReadinessProbe: func(_ fiber.Ctx) bool { + })) + app.Get("/probe/ready", NewHealthChecker(Config{ + Probe: func(_ fiber.Ctx) bool { select { case <-c1: return true @@ -181,7 +122,6 @@ func Test_HealthCheck_Custom_Nested(t *testing.T) { return false } }, - ReadinessEndpoint: "/probe/ready", })) // Testing custom health check endpoints with nested paths @@ -209,12 +149,17 @@ func Test_HealthCheck_Next(t *testing.T) { app := fiber.New() - app.Use(New(Config{ + checker := NewHealthChecker(Config{ Next: func(_ fiber.Ctx) bool { return true }, - })) + }) + app.Get("/readyz", checker) + app.Get("/livez", checker) + + // This should give not found since there are no other handlers to execute + // so it's like the route isn't defined at all shouldGiveNotFound(t, app, "/readyz") shouldGiveNotFound(t, app, "/livez") } @@ -222,7 +167,8 @@ func Test_HealthCheck_Next(t *testing.T) { func Benchmark_HealthCheck(b *testing.B) { app := fiber.New() - app.Use(New()) + app.Get(DefaultLivenessEndpoint, NewHealthChecker()) + app.Get(DefaultReadinessEndpoint, NewHealthChecker()) h := app.Handler() fctx := &fasthttp.RequestCtx{} @@ -238,3 +184,25 @@ func Benchmark_HealthCheck(b *testing.B) { require.Equal(b, fiber.StatusOK, fctx.Response.Header.StatusCode()) } + +func Benchmark_HealthCheck_Parallel(b *testing.B) { + app := fiber.New() + + app.Get(DefaultLivenessEndpoint, NewHealthChecker()) + app.Get(DefaultReadinessEndpoint, NewHealthChecker()) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/livez") + + for pb.Next() { + h(fctx) + } + }) +}