Skip to content

Commit

Permalink
✨ feat: add liveness and readiness checks (#2509)
Browse files Browse the repository at this point in the history
* ✨ feat: add liveness and readiness checkers

* 📝 docs: add docs for liveness and readiness

* ✨ feat: add options method for probe checkers

* ✅ tests: add tests for liveness and readiness

* ♻️ refactor: change default endpoint values

* ♻️ refactor: change default value for liveness endpoint

* 📝 docs: add return status for liveness and readiness probes

* ♻️ refactor: change probechecker to middleware

* 📝 docs: move docs to middleware session

* ♻️ refactor: apply gofumpt formatting

* ♻️ refactor: remove unused parameter

* split config and apply a review

* apply reviews and add testcases

* add benchmark

* cleanup

* rename middleware

* fix linter

* Update docs and config values

* Revert change to IsReady

* Updates based on code review

* Update docs to match other middlewares

---------

Co-authored-by: Muhammed Efe Cetin <efectn@protonmail.com>
Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com>
  • Loading branch information
4 people authored Jan 3, 2024
1 parent 38eb4bd commit 6249bc4
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -654,4 +654,4 @@ Hooks is a method to return [hooks](../guide/hooks.md) property.

```go title="Signature"
func (app *App) Hooks() *Hooks
```
```
105 changes: 105 additions & 0 deletions docs/api/middleware/healthcheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
id: healthcheck
title: healthcheck
---

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.

## Overview

- **Liveness Probe**: Checks if the server is up and running.
- **Default Endpoint**: `/livez`
- **Behavior**: By default returns `true` immediately when the server is operational.

- **Readiness Probe**: Assesses if the application is ready to handle requests.
- **Default Endpoint**: `/readyz`
- **Behavior**: By default returns `true` immediately when the server is operational.

- **HTTP Status Codes**:
- `200 OK`: Returned when the checker function evaluates to `true`.
- `503 Service Unavailable`: Returned when the checker function evaluates to `false`.

## Signatures

```go
func New(config Config) fiber.Handler
```

## Examples

Import the middleware package that is part of the Fiber web framework
```go
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/healthcheck"
)
```

After you initiate your Fiber app, you can use the following possibilities:

```go
// Provide a minimal config
app.Use(healthcheck.New())

// Or extend your config for customization
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",
}))
```

## Config

```go
type Config struct {
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool

// 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
// 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
}
```

## Default Config

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 }

var ConfigDefault = Config{
LivenessProbe: defaultLivenessProbe,
ReadinessProbe: defaultReadinessProbe,
LivenessEndpoint: "/livez",
ReadinessEndpoint: "/readyz",
}
```
84 changes: 84 additions & 0 deletions middleware/healthcheck/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package healthcheck

import (
"github.com/gofiber/fiber/v2"
)

// Config defines the configuration options for the healthcheck middleware.
type Config struct {
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool

// 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
// 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
}

const (
DefaultLivenessEndpoint = "/livez"
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,
}

// defaultConfig returns a default config for the healthcheck middleware.
func defaultConfig(config ...Config) Config {
if len(config) < 1 {
return ConfigDefault
}

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
}

return cfg
}
52 changes: 52 additions & 0 deletions middleware/healthcheck/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package healthcheck

import (
"github.com/gofiber/fiber/v2"
)

// HealthChecker defines a function to check liveness or readiness of the application
type HealthChecker func(*fiber.Ctx) bool

// ProbeCheckerHandler defines a function that returns a ProbeChecker
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)

return func(c *fiber.Ctx) error {
// Don't execute middleware if Next returns true
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}

if c.Method() != fiber.MethodGet {
return c.Next()
}

switch c.Path() {
case cfg.ReadinessEndpoint:
return isReadyHandler(c)
case cfg.LivenessEndpoint:
return isLiveHandler(c)
}

return c.Next()
}
}
117 changes: 117 additions & 0 deletions middleware/healthcheck/healthcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package healthcheck

import (
"net/http/httptest"
"testing"
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
)

func Test_HealthCheck_Default(t *testing.T) {
t.Parallel()

app := fiber.New()
app.Use(New())

req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/readyz", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusOK, req.StatusCode)

req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusOK, req.StatusCode)
}

func Test_HealthCheck_Custom(t *testing.T) {
t.Parallel()

app := fiber.New()

c1 := make(chan struct{}, 1)
go func() {
time.Sleep(1 * time.Second)
c1 <- struct{}{}
}()

app.Use(New(Config{
LivenessProbe: func(c *fiber.Ctx) bool {
return true
},
LivenessEndpoint: "/live",
ReadinessProbe: func(c *fiber.Ctx) bool {
select {
case <-c1:
return true
default:
return false
}
},
ReadinessEndpoint: "/ready",
}))

// Live should return 200 with GET request
req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/live", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusOK, req.StatusCode)

// Live should return 404 with POST request
req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode)

// Ready should return 404 with POST request
req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/ready", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode)

// Ready should return 503 with GET request before the channel is closed
req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusServiceUnavailable, req.StatusCode)

time.Sleep(1 * time.Second)

// Ready should return 200 with GET request after the channel is closed
req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusOK, req.StatusCode)
}

func Test_HealthCheck_Next(t *testing.T) {
t.Parallel()

app := fiber.New()

app.Use(New(Config{
Next: func(c *fiber.Ctx) bool {
return true
},
}))

req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode)
}

func Benchmark_HealthCheck(b *testing.B) {
app := fiber.New()

app.Use(New())

h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI("/livez")

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
h(fctx)
}

utils.AssertEqual(b, fiber.StatusOK, fctx.Response.Header.StatusCode())
}

0 comments on commit 6249bc4

Please sign in to comment.