Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion health/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Check struct {
state *State
}

// NewCheck creates a new Check
// NewCheck creates a new Check. Every check should have a unique name.
//
// You are able to return custom statuses by returning a StatusError from the check function. This way you can perform
// checks that return a status other than up or down. For example, you can return a status of "degraded" if the check
Expand Down
80 changes: 45 additions & 35 deletions health/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,38 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
)

// Checker is an interface that defines a health checker.
type Checker interface {
// Handler returns the handler for the check.
Handler() http.HandlerFunc

// Check returns the result of the check.
Check(ctx context.Context) *Result
}

// checker is a struct that implements the Checker interface.
// Checker is a struct that handles the checking of multiple health checks.
//
// This is a group of checks that can be run in parallel.
type checker struct {
baseCtx context.Context
cancel context.CancelFunc
mtx *sync.Mutex
checks []*Check
type Checker struct {
checks sync.Map

httpStatusCodeUp int
httpStatusCodeDown int
}

// NewChecker creates a new Checker.
func NewChecker(opts ...CheckerOption) Checker {
c := &checker{
mtx: new(sync.Mutex),
checks: make([]*Check, 0),
func NewChecker(opts ...CheckerOption) (*Checker, error) {
c := &Checker{
httpStatusCodeUp: http.StatusOK,
httpStatusCodeDown: http.StatusServiceUnavailable,
}

for _, opt := range opts {
opt(c)
}

if c.baseCtx == nil {
c.baseCtx, c.cancel = context.WithCancel(context.Background())
if err := opt(c); err != nil {
return nil, fmt.Errorf("failed to apply checker option: %w", err)
}
}

return c
return c, nil
}

func (c *checker) httpCodeFromStatus(status Status) int {
func (c *Checker) httpCodeFromStatus(status Status) int {
switch status {
case StatusUp:
return c.httpStatusCodeUp
Expand All @@ -62,7 +47,7 @@ func (c *checker) httpCodeFromStatus(status Status) int {
}

// Handler returns the handler for the check.
func (c *checker) Handler() http.HandlerFunc {
func (c *Checker) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
result := c.Check(r.Context())
httpStatus := c.httpCodeFromStatus(result.Status)
Expand All @@ -73,21 +58,25 @@ func (c *checker) Handler() http.HandlerFunc {
}

// Check returns the result of the check.
func (c *checker) Check(ctx context.Context) *Result {
c.mtx.Lock()
defer c.mtx.Unlock()

func (c *Checker) Check(ctx context.Context) *Result {
if ctx == nil {
ctx = context.Background()
}

result := NewResult()

wg := new(sync.WaitGroup)
for _, check := range c.checks {
c.checks.Range(func(key, value any) bool {
check, ok := value.(*Check)
if !ok {
// This "should" never happen, but just in case
return true
}

wg.Add(1)
go func(check *Check, result *Result) {
go func(check *Check) {
defer wg.Done()

checkResult := NewResult()

checkStatus := StatusUp
Expand All @@ -104,9 +93,30 @@ func (c *checker) Check(ctx context.Context) *Result {

result.SetStatus(checkResult.Status)
result.addDetail(check.String(), checkResult)
}(check, result)
}
}(check)

return true
})
wg.Wait()

return result
}

// AddCheck adds a check to the checker.
func (c *Checker) AddCheck(check *Check) error {
if check == nil {
return errors.New("check is nil")
}

if check.name == "" {
return errors.New("check name is empty")
}

if _, ok := c.checks.Load(check.String()); ok {
return fmt.Errorf("check already exists with the same key: %s", check.String())
}

c.checks.Store(check.String(), check)

return nil
}
43 changes: 18 additions & 25 deletions health/checker_options.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,45 @@
package health

import (
"context"
)

type CheckerOption func(*checker)

// WithCheckerBaseContext sets the base context for the checker.
func WithCheckerBaseContext(baseCtx context.Context) CheckerOption {
return func(c *checker) {
ctx, cancel := context.WithCancel(baseCtx)
c.baseCtx = ctx
c.cancel = cancel
}
}
import "fmt"

type CheckerOption func(*Checker) error

// WithCheckerCheck adds a single check to the checker.
func WithCheckerCheck(check *Check) CheckerOption {
return func(c *checker) {
if check == nil {
return
return func(c *Checker) error {
if err := c.AddCheck(check); err != nil {
return fmt.Errorf("failed to add check: %w", err)
}
if c.checks == nil {
c.checks = make([]*Check, 0)
}
c.checks = append(c.checks, check)

return nil
}
}

// WithCheckerChecks adds multiple checks to the checker.
func WithCheckerChecks(checks ...*Check) CheckerOption {
return func(c *checker) {
return func(c *Checker) error {
for _, check := range checks {
WithCheckerCheck(check)(c)
if err := WithCheckerCheck(check)(c); err != nil {
return fmt.Errorf("failed to add check %s: %w", check.String(), err)
}
}

return nil
}
}

// WithCheckerHTTPCodeUp sets the HTTP status code when the system is up.
func WithCheckerHTTPCodeUp(code int) CheckerOption {
return func(c *checker) {
return func(c *Checker) error {
c.httpStatusCodeUp = code
return nil
}
}

// WithCheckerHTTPCodeDown sets the HTTP status code when the system is down.
func WithCheckerHTTPCodeDown(code int) CheckerOption {
return func(c *checker) {
return func(c *Checker) error {
c.httpStatusCodeDown = code
return nil
}
}
98 changes: 84 additions & 14 deletions health/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ func TestNewChecker(t *testing.T) {
return nil
})

got := NewChecker(WithCheckerCheck(gotCheck))
c, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

c, ok := got.(*checker)
require.True(t, ok, "NewChecker() should return a *checker")
require.NotNil(t, c)
require.Equal(t, http.StatusOK, c.httpStatusCodeUp)
require.Equal(t, http.StatusServiceUnavailable, c.httpStatusCodeDown)
require.NotNil(t, c.baseCtx, "NewChecker() should set the base context")
}

func TestNewCheckerHandler_Single(t *testing.T) {
Expand All @@ -36,7 +34,8 @@ func TestNewCheckerHandler_Single(t *testing.T) {
return nil
})

got := NewChecker(WithCheckerCheck(gotCheck))
got, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand All @@ -62,7 +61,8 @@ func TestNewCheckerHandler_Single_StatusError(t *testing.T) {
return NewStatusError(errors.New("test error"), StatusDegraded)
})

got := NewChecker(WithCheckerCheck(gotCheck))
got, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand All @@ -88,7 +88,8 @@ func TestNewCheckerHandler_Single_StatusError_InvalidStatus(t *testing.T) {
return NewStatusError(errors.New("test error"), 123)
})

got := NewChecker(WithCheckerCheck(gotCheck))
got, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand Down Expand Up @@ -118,7 +119,8 @@ func TestNewCheckerHandler_Multiple(t *testing.T) {
return nil
})

got := NewChecker(WithCheckerChecks([]*Check{gotCheck, secondCheck}...))
got, err := NewChecker(WithCheckerChecks([]*Check{gotCheck, secondCheck}...))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand All @@ -144,7 +146,8 @@ func TestNewCheckerHandler_Single_Error(t *testing.T) {
return errors.New("test error")
})

got := NewChecker(WithCheckerCheck(gotCheck))
got, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand All @@ -170,7 +173,8 @@ func TestNewCheckerHandler_NoParentContext(t *testing.T) {
return nil
})

got := NewChecker(WithCheckerCheck(gotCheck))
got, err := NewChecker(WithCheckerCheck(gotCheck))
require.NoError(t, err)

handler := got.Handler()
require.NotNil(t, handler)
Expand Down Expand Up @@ -224,12 +228,78 @@ func TestChecker_HttpCodeFromStatus(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got := NewChecker()
c, ok := got.(*checker)
require.True(t, ok, "NewChecker() should return a *checker")
c, err := NewChecker()
require.NoError(t, err)
require.NotNil(t, c)

require.Equal(t, tt.expectedStatus, c.httpCodeFromStatus(tt.status))
})
}
}

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

c, err := NewChecker()
require.NoError(t, err)
require.NotNil(t, c)

check := NewCheck("test_check", func(_ context.Context) error {
return nil
})

err = c.AddCheck(check)
require.NoError(t, err)

// Check if the check was added
c.checks.Range(func(key, value any) bool {
require.Equal(t, "test_check", key)
return false
})
}

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

c, err := NewChecker()
require.NoError(t, err)
require.NotNil(t, c)

err = c.AddCheck(nil)
require.Error(t, err)
require.Equal(t, "check is nil", err.Error())
}

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

c, err := NewChecker()
require.NoError(t, err)
require.NotNil(t, c)

check := NewCheck("", func(_ context.Context) error {
return nil
})

err = c.AddCheck(check)
require.Error(t, err)
require.Equal(t, "check name is empty", err.Error())
}

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

c, err := NewChecker()
require.NoError(t, err)
require.NotNil(t, c)

check := NewCheck("test_check", func(_ context.Context) error {
return nil
})

err = c.AddCheck(check)
require.NoError(t, err)

err = c.AddCheck(check)
require.Error(t, err)
require.Equal(t, "check already exists with the same key: test_check", err.Error())
}
Loading