Skip to content

Commit

Permalink
Merge pull request #4 from kazhuravlev/ka/gh-2
Browse files Browse the repository at this point in the history
Store status changes for each check
  • Loading branch information
kazhuravlev authored Jun 23, 2024
2 parents 8b57c35 + e21a3b2 commit 6d1754c
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 200 deletions.
30 changes: 15 additions & 15 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import (
"context"
"log/slog"
"sync"

"github.com/kazhuravlev/just"
)

// Register will register a check.
//
// All checks should have a name. Will be better that name will contain only lowercase symbols and lodash.
// This is allowing to have the same name for CheckStatus and for metrics.
// This is allowing to have the same name for Check and for metrics.
func (s *Healthcheck) Register(ctx context.Context, check ICheck) {
s.checksMu.Lock()
defer s.checksMu.Unlock()
Expand Down Expand Up @@ -41,10 +39,9 @@ CheckID:
check.run()
}

s.checks = append(s.checks, checkRec{
ID: checkID,
CheckFn: check.check,
Timeout: check.timeout(),
s.checks = append(s.checks, checkContainer{
ID: checkID,
Check: check,
})
}

Expand All @@ -53,27 +50,30 @@ func (s *Healthcheck) RunAllChecks(ctx context.Context) Report {
s.checksMu.RLock()
defer s.checksMu.RUnlock()

checks := make([]CheckStatus, len(s.checks))
checks := make([]Check, len(s.checks))
{
wg := new(sync.WaitGroup)
wg.Add(len(s.checks))

// TODO(zhuravlev): do not run goroutines for checks like manual and bg check.
for i := range s.checks {
go func(i int, check checkRec) {
go func(i int, check checkContainer) {
defer wg.Done()

checks[i] = runCheck(ctx, s.opts, check)
checks[i] = s.runCheck(ctx, check)
}(i, s.checks[i])
}

wg.Wait()
}

failedChecks := just.SliceFilter(checks, func(s CheckStatus) bool {
return s.Status == StatusDown
})

status := just.If(len(failedChecks) == 0, StatusUp, StatusDown)
status := StatusUp
for _, check := range checks {
if check.State.Status == StatusDown {
status = StatusDown
break
}
}

return Report{
Status: status,
Expand Down
106 changes: 73 additions & 33 deletions api_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ package healthcheck
import (
"context"
"errors"
"sync"
"github.com/kazhuravlev/healthcheck/internal/logr"
"time"
)

var (
_ ICheck = (*basicCheck)(nil)
_ ICheck = (*manualCheck)(nil)
_ ICheck = (*bgCheck)(nil)
)

// errInitial used as initial error for some checks.
var errInitial = errors.New("initial")

type basicCheck struct {
name string
ttl time.Duration
fn CheckFn
logg *logr.Ring
}

// NewBasic creates a basic check. This check will only be performed when RunAllChecks is called.
Expand All @@ -22,18 +32,28 @@ func NewBasic(name string, timeout time.Duration, fn CheckFn) *basicCheck {
name: name,
ttl: timeout,
fn: fn,
logg: logr.New(),
}
}

func (c *basicCheck) id() string { return c.name }
func (c *basicCheck) timeout() time.Duration { return c.ttl }
func (c *basicCheck) check(ctx context.Context) result { return result{Err: c.fn(ctx)} }
func (c *basicCheck) id() string { return c.name }
func (c *basicCheck) timeout() time.Duration { return c.ttl }
func (c *basicCheck) check(ctx context.Context) logr.Rec {
res := logr.Rec{
Time: time.Now(),
Error: c.fn(ctx),
}
c.logg.Put(res)

return res
}
func (c *basicCheck) log() []logr.Rec {
return c.logg.SlicePrev()
}

type manualCheck struct {
name string

mu *sync.RWMutex
err error
logg *logr.Ring
}

// NewManual create new check, that can be managed by client. Marked as failed by default.
Expand All @@ -44,27 +64,35 @@ type manualCheck struct {
// hc.Register(check)
// check.SetError(errors.New("service unavailable"))
func NewManual(name string) *manualCheck {
return &manualCheck{
check := &manualCheck{
name: name,
mu: new(sync.RWMutex),
err: errors.New("initial status"), //nolint:goerr113 // This error should not be handled
logg: logr.New(),
}

check.SetErr(errInitial)

return check
}

func (c *manualCheck) SetErr(err error) {
c.mu.Lock()
defer c.mu.Unlock()

c.err = err
c.logg.Put(logr.Rec{
Time: time.Now(),
Error: err,
})
}

func (c *manualCheck) id() string { return c.name }
func (c *manualCheck) timeout() time.Duration { return time.Hour }
func (c *manualCheck) check(_ context.Context) result {
c.mu.RLock()
defer c.mu.RUnlock()
func (c *manualCheck) check(_ context.Context) logr.Rec {
rec, ok := c.logg.GetLast()
if !ok {
panic("manual check must have initial state")
}

return result{Err: c.err}
return rec
}
func (c *manualCheck) log() []logr.Rec {
return c.logg.SlicePrev()
}

type bgCheck struct {
Expand All @@ -73,9 +101,7 @@ type bgCheck struct {
delay time.Duration
ttl time.Duration
fn CheckFn

muErr *sync.RWMutex
err error
logg *logr.Ring
}

// NewBackground will create a check that runs in background. Usually used for slow or expensive checks.
Expand All @@ -84,16 +110,21 @@ type bgCheck struct {
// hc, _ := healthcheck.New(...)
// hc.Register(healthcheck.NewBackground("some_subsystem"))
func NewBackground(name string, initialErr error, delay, period, timeout time.Duration, fn CheckFn) *bgCheck {
return &bgCheck{
name: name,

check := &bgCheck{
name: name,
period: period,
delay: delay,
ttl: timeout,
fn: fn,
muErr: new(sync.RWMutex),
err: initialErr,
logg: logr.New(),
}

check.logg.Put(logr.Rec{
Time: time.Now(),
Error: initialErr,
})

return check
}

func (c *bgCheck) run() {
Expand All @@ -110,9 +141,10 @@ func (c *bgCheck) run() {

err := c.fn(ctx)

c.muErr.Lock()
c.err = err
c.muErr.Unlock()
c.logg.Put(logr.Rec{
Time: time.Now(),
Error: err,
})
}()

select {
Expand All @@ -124,9 +156,17 @@ func (c *bgCheck) run() {

func (c *bgCheck) id() string { return c.name }
func (c *bgCheck) timeout() time.Duration { return time.Hour }
func (c *bgCheck) check(_ context.Context) result {
c.muErr.RLock()
defer c.muErr.RUnlock()
func (c *bgCheck) check(_ context.Context) logr.Rec {
val, ok := c.logg.GetLast()
if !ok {
return logr.Rec{
Time: time.Now(),
Error: nil,
}
}

return result{Err: c.err}
return val
}
func (c *bgCheck) log() []logr.Rec {
return c.logg.SlicePrev()
}
23 changes: 23 additions & 0 deletions api_checks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package healthcheck

import (
"context"
"github.com/stretchr/testify/require"
"testing"
"time"
)

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

t.Run("check_constructor", func(t *testing.T) {
t.Parallel()

check := NewManual("sample")

require.Equal(t, "sample", check.id())
require.Equal(t, time.Hour, check.timeout())
require.ErrorIs(t, check.check(context.TODO()).Error, errInitial)
require.Len(t, check.log(), 0)
})
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@ go 1.22.3
require (
github.com/kazhuravlev/just v0.70.0
github.com/prometheus/client_golang v1.19.1
github.com/stretchr/testify v1.9.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/go-playground/validator/v10 v10.21.0 // indirect
github.com/goccy/go-yaml v1.11.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
Expand All @@ -20,6 +21,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kazhuravlev/just v0.70.0 h1:Dkakxq943SQ6ratRC5O6gxSBEg/3FyTqGNrLmiqrHAw=
github.com/kazhuravlev/just v0.70.0/go.mod h1:0R3XZwnkP5zvLbQ+ARMopVWSfI17Q+j/8y7EhvbWl0w=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand All @@ -37,6 +42,8 @@ github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
Expand All @@ -55,5 +62,8 @@ golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSm
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 2 additions & 2 deletions healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Healthcheck struct {
opts hcOptions

checksMu *sync.RWMutex
checks []checkRec
checks []checkContainer
}

func New(opts ...func(*hcOptions)) (*Healthcheck, error) {
Expand All @@ -24,7 +24,7 @@ func New(opts ...func(*hcOptions)) (*Healthcheck, error) {

return &Healthcheck{
opts: options,
checks: nil,
checksMu: new(sync.RWMutex),
checks: nil,
}, nil
}
Loading

0 comments on commit 6d1754c

Please sign in to comment.