feat: Add Startup Probe to Healthcheck Middleware (#3069)

* added startup default probe endpoint

* added test case

* updated docs

* updated test order

* added test case

* fixed go fmt and md lint

* fixed go fmt and md lint

* updated doc as per coderabbitai suggestions

* changed healhtcheck route register to use default const instead of string for test cases

* updated whats new with healthcheck content

* updated whats new doc with coderabbitai sugg

* updated migration guide
pull/3076/head
kirankumar-grootan 2024-07-18 17:24:44 +05:30 committed by Juan Calderon-Perez
parent 6d41e97c2c
commit 7156af655a
4 changed files with 108 additions and 7 deletions

View File

@ -4,7 +4,7 @@ id: healthcheck
# Health Check # Health Check
Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides two endpoints for checking the liveness and readiness state of HTTP applications. Liveness, readiness and startup probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides three endpoints for checking the liveness, readiness, and startup state of HTTP applications.
## Overview ## Overview
@ -16,6 +16,10 @@ Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/
- **Default Endpoint**: `/readyz` - **Default Endpoint**: `/readyz`
- **Behavior**: By default returns `true` immediately when the server is operational. - **Behavior**: By default returns `true` immediately when the server is operational.
- **Startup Probe**: Checks if the application has completed its startup sequence and is ready to proceed with initialization and readiness checks.
- **Default Endpoint**: `/startupz`
- **Behavior**: By default returns `true` immediately when the server is operational.
- **HTTP Status Codes**: - **HTTP Status Codes**:
- `200 OK`: Returned when the checker function evaluates to `true`. - `200 OK`: Returned when the checker function evaluates to `true`.
- `503 Service Unavailable`: Returned when the checker function evaluates to `false`. - `503 Service Unavailable`: Returned when the checker function evaluates to `false`.
@ -44,6 +48,8 @@ After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can u
app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker()) app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker())
// Provide a minimal config for readiness check // Provide a minimal config for readiness check
app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker())
// Provide a minimal config for startup check
app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker())
// Provide a minimal config for check with custom endpoint // Provide a minimal config for check with custom endpoint
app.Get("/live", healthcheck.NewHealthChecker()) app.Get("/live", healthcheck.NewHealthChecker())
@ -59,6 +65,12 @@ app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker(healt
return true return true
}, },
})) }))
// And it works the same for startup, just change the route
app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{
Probe: func(c fiber.Ctx) bool {
return true
},
}))
// With a custom route and custom probe // With a custom route and custom probe
app.Get("/live", healthcheck.NewHealthChecker(healthcheck.Config{ app.Get("/live", healthcheck.NewHealthChecker(healthcheck.Config{
Probe: func(c fiber.Ctx) bool { Probe: func(c fiber.Ctx) bool {
@ -90,6 +102,10 @@ type Config struct {
// Function used for checking the liveness of the application. Returns true if the application // Function used for checking the liveness of the application. Returns true if the application
// is running and false if it is not. The liveness probe is typically used to indicate if // is running and false if it is not. The liveness probe is typically used to indicate if
// the application is in a state where it can handle requests (e.g., the server is up and running). // the application is in a state where it can handle requests (e.g., the server is up and running).
// The readiness probe is typically used to indicate if the application is ready to start accepting traffic (e.g., all necessary components
// are initialized and dependent services are available) and the startup probe typically used to
// indicate if the application has completed its startup sequence and is ready to proceed with
// initialization and readiness checks
// //
// Optional. Default: func(c fiber.Ctx) bool { return true } // Optional. Default: func(c fiber.Ctx) bool { return true }
Probe HealthChecker Probe HealthChecker

View File

@ -33,6 +33,7 @@ Here's a quick overview of the changes in Fiber `v3`:
- [Session](#session) - [Session](#session)
- [Filesystem](#filesystem) - [Filesystem](#filesystem)
- [Monitor](#monitor) - [Monitor](#monitor)
- [Healthcheck](#healthcheck)
- [📋 Migration guide](#-migration-guide) - [📋 Migration guide](#-migration-guide)
## Drop for old Go versions ## Drop for old Go versions
@ -342,6 +343,25 @@ DRAFT section
Monitor middleware is now in Contrib package. Monitor middleware is now in Contrib package.
### Healthcheck
The Healthcheck middleware has been enhanced to support more than two routes, with default endpoints for liveliness, readiness, and startup checks. Here's a detailed breakdown of the changes and how to use the new features.
1. **Support for More Than Two Routes**:
- The updated middleware now supports multiple routes beyond the default liveliness and readiness endpoints. This allows for more granular health checks, such as startup probes.
2. **Default Endpoints**:
- Three default endpoints are now available:
- **Liveness**: `/livez`
- **Readiness**: `/readyz`
- **Startup**: `/startupz`
- These endpoints can be customized or replaced with user-defined routes.
3. **Simplified Configuration**:
- The configuration for each health check endpoint has been simplified. Each endpoint can be configured separately, allowing for more flexibility and readability.
Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes.
## 📋 Migration guide ## 📋 Migration guide
- [🚀 App](#-app-1) - [🚀 App](#-app-1)
@ -492,3 +512,48 @@ app.Use(static.New("", static.Config{
MaxAge: 3600, MaxAge: 3600,
})) }))
``` ```
### Healthcheck
Previously, the Healthcheck middleware was configured with a combined setup for liveliness and readiness probes:
```go
//before
app.Use(healthcheck.New(healthcheck.Config{
LivenessProbe: func(c *fiber.Ctx) bool {
return true
},
LivenessEndpoint: "/live",
ReadinessProbe: func(c *fiber.Ctx) bool {
return serviceA.Ready() && serviceB.Ready() && ...
},
ReadinessEndpoint: "/ready",
}))
```
With the new version, each health check endpoint is configured separately, allowing for more flexibility:
```go
// after
// Default liveness endpoint configuration
app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{
Probe: func(c *fiber.Ctx) bool {
return true
},
}))
// Default readiness endpoint configuration
app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker())
// New default startup endpoint configuration
// Default endpoint is /startupz
app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{
Probe: func(c *fiber.Ctx) bool {
return serviceA.Ready() && serviceB.Ready() && ...
},
}))
// Custom liveness endpoint configuration
app.Get("/live", healthcheck.NewHealthChecker())
```

View File

@ -24,6 +24,7 @@ type Config struct {
const ( const (
DefaultLivenessEndpoint = "/livez" DefaultLivenessEndpoint = "/livez"
DefaultReadinessEndpoint = "/readyz" DefaultReadinessEndpoint = "/readyz"
DefaultStartupEndpoint = "/startupz"
) )
func defaultProbe(fiber.Ctx) bool { return true } func defaultProbe(fiber.Ctx) bool { return true }

View File

@ -34,30 +34,38 @@ func Test_HealthCheck_Strict_Routing_Default(t *testing.T) {
StrictRouting: true, StrictRouting: true,
}) })
app.Get("/livez", NewHealthChecker()) app.Get(DefaultLivenessEndpoint, NewHealthChecker())
app.Get("/readyz", NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker())
app.Get(DefaultStartupEndpoint, NewHealthChecker())
shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/readyz")
shouldGiveOK(t, app, "/livez") shouldGiveOK(t, app, "/livez")
shouldGiveOK(t, app, "/startupz")
shouldGiveNotFound(t, app, "/readyz/") shouldGiveNotFound(t, app, "/readyz/")
shouldGiveNotFound(t, app, "/livez/") shouldGiveNotFound(t, app, "/livez/")
shouldGiveNotFound(t, app, "/startupz/")
shouldGiveNotFound(t, app, "/notDefined/readyz") shouldGiveNotFound(t, app, "/notDefined/readyz")
shouldGiveNotFound(t, app, "/notDefined/livez") shouldGiveNotFound(t, app, "/notDefined/livez")
shouldGiveNotFound(t, app, "/notDefined/startupz")
} }
func Test_HealthCheck_Default(t *testing.T) { func Test_HealthCheck_Default(t *testing.T) {
t.Parallel() t.Parallel()
app := fiber.New() app := fiber.New()
app.Get("/livez", NewHealthChecker()) app.Get(DefaultLivenessEndpoint, NewHealthChecker())
app.Get("/readyz", NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker())
app.Get(DefaultStartupEndpoint, NewHealthChecker())
shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/readyz")
shouldGiveOK(t, app, "/livez") shouldGiveOK(t, app, "/livez")
shouldGiveOK(t, app, "/startupz")
shouldGiveOK(t, app, "/readyz/") shouldGiveOK(t, app, "/readyz/")
shouldGiveOK(t, app, "/livez/") shouldGiveOK(t, app, "/livez/")
shouldGiveOK(t, app, "/startupz/")
shouldGiveNotFound(t, app, "/notDefined/readyz") shouldGiveNotFound(t, app, "/notDefined/readyz")
shouldGiveNotFound(t, app, "/notDefined/livez") shouldGiveNotFound(t, app, "/notDefined/livez")
shouldGiveNotFound(t, app, "/notDefined/startupz")
} }
func Test_HealthCheck_Custom(t *testing.T) { func Test_HealthCheck_Custom(t *testing.T) {
@ -80,6 +88,11 @@ func Test_HealthCheck_Custom(t *testing.T) {
} }
}, },
})) }))
app.Get(DefaultStartupEndpoint, NewHealthChecker(Config{
Probe: func(_ fiber.Ctx) bool {
return false
},
}))
// Setup custom liveness and readiness probes to simulate application health status // Setup custom liveness and readiness probes to simulate application health status
// Live should return 200 with GET request // Live should return 200 with GET request
@ -97,6 +110,8 @@ func Test_HealthCheck_Custom(t *testing.T) {
// Ready should return 503 with GET request before the channel is closed // Ready should return 503 with GET request before the channel is closed
shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable) shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable)
shouldGiveStatus(t, app, "/startupz", fiber.StatusServiceUnavailable)
// Ready should return 200 with GET request after the channel is closed // Ready should return 200 with GET request after the channel is closed
c1 <- struct{}{} c1 <- struct{}{}
shouldGiveOK(t, app, "/ready") shouldGiveOK(t, app, "/ready")
@ -155,13 +170,15 @@ func Test_HealthCheck_Next(t *testing.T) {
}, },
}) })
app.Get("/readyz", checker) app.Get(DefaultLivenessEndpoint, checker)
app.Get("/livez", checker) app.Get(DefaultReadinessEndpoint, checker)
app.Get(DefaultStartupEndpoint, checker)
// This should give not found since there are no other handlers to execute // This should give not found since there are no other handlers to execute
// so it's like the route isn't defined at all // so it's like the route isn't defined at all
shouldGiveNotFound(t, app, "/readyz") shouldGiveNotFound(t, app, "/readyz")
shouldGiveNotFound(t, app, "/livez") shouldGiveNotFound(t, app, "/livez")
shouldGiveNotFound(t, app, "/startupz")
} }
func Benchmark_HealthCheck(b *testing.B) { func Benchmark_HealthCheck(b *testing.B) {
@ -169,6 +186,7 @@ func Benchmark_HealthCheck(b *testing.B) {
app.Get(DefaultLivenessEndpoint, NewHealthChecker()) app.Get(DefaultLivenessEndpoint, NewHealthChecker())
app.Get(DefaultReadinessEndpoint, NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker())
app.Get(DefaultStartupEndpoint, NewHealthChecker())
h := app.Handler() h := app.Handler()
fctx := &fasthttp.RequestCtx{} fctx := &fasthttp.RequestCtx{}
@ -190,6 +208,7 @@ func Benchmark_HealthCheck_Parallel(b *testing.B) {
app.Get(DefaultLivenessEndpoint, NewHealthChecker()) app.Get(DefaultLivenessEndpoint, NewHealthChecker())
app.Get(DefaultReadinessEndpoint, NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker())
app.Get(DefaultStartupEndpoint, NewHealthChecker())
h := app.Handler() h := app.Handler()