Skip to content

Commit

Permalink
Merge 04fc5d0 into e6ac327
Browse files Browse the repository at this point in the history
  • Loading branch information
dmarkhas committed May 25, 2021
2 parents e6ac327 + 04fc5d0 commit 3ecbb7e
Show file tree
Hide file tree
Showing 17 changed files with 133 additions and 74 deletions.
18 changes: 11 additions & 7 deletions README.md
Expand Up @@ -105,7 +105,7 @@ Creating a host lookup check is easy:
```go
// Schedule a host resolution check for `example.com`, requiring at least one results, and running every 10 sec
h.RegisterCheck(
checks.NewHostResolveCheck("example.com", 200*time.Millisecond, 1),
checks.NewHostResolveCheck("example.com", 1),
gosundheit.ExecutionPeriod(10 * time.Second),
)
```
Expand All @@ -122,8 +122,9 @@ func ReverseDNLookup(ctx context.Context, addr string) (resolvedCount int, err e
//...

h.RegisterCheck(
checks.NewResolveCheck(ReverseDNLookup, "127.0.0.1", 200*time.Millisecond, 3),
checks.NewResolveCheck(ReverseDNLookup, "127.0.0.1", 3),
gosundheit.ExecutionPeriod(10 * time.Second),
gosundheit.ExecutionTimeout(1*time.Second)
)
```

Expand All @@ -132,7 +133,7 @@ The ping checks allow you to verifies that a resource is still alive and reachab
For example, you can use it as a DB ping check (`sql.DB` implements the Pinger interface):
```go
db, err := sql.Open(...)
dbCheck, err := checks.NewPingCheck("db.check", db, time.Millisecond*100)
dbCheck, err := checks.NewPingCheck("db.check", db)
_ = h.RegisterCheck(&gosundheit.Config{
Check: dbCheck,
// ...
Expand All @@ -142,7 +143,7 @@ For example, you can use it as a DB ping check (`sql.DB` implements the Pinger i
You can also use the ping check to test a generic connection like so:
```go
pinger := checks.NewDialPinger("tcp", "example.com")
pingCheck, err := checks.NewPingCheck("example.com.reachable", pinger, time.Second)
pingCheck, err := checks.NewPingCheck("example.com.reachable", pinger)
h.RegisterCheck(pingCheck)
```

Expand Down Expand Up @@ -180,7 +181,7 @@ func lotteryCheck() (details interface{}, err error) {
}
```

Now we register the check to start running right away, and execute once per 2 minutes:
Now we register the check to start running right away, and execute once per 2 minutes with a timeout of 5 seconds:
```go
h := gosundheit.New()
...
Expand All @@ -191,7 +192,8 @@ h.RegisterCheck(
CheckFunc: lotteryCheck,
},
gosundheit.InitialDelay(0),
gosundheit.ExecutionPeriod(2 * time.Minute),
gosundheit.ExecutionPeriod(2 * time.Minute),
gosundheit.ExecutionTimeout(5 * time.Second)
)
```

Expand Down Expand Up @@ -221,7 +223,7 @@ func (l Lottery) Name() string {
}
```

And register our custom check, scheduling it to run after 1 sec, and every 30 sec:
And register our custom check, scheduling it to run every 30 seconds (after a 1 second initial delay) with a 5 seconds timeout:
```go
h := gosundheit.New()
...
Expand All @@ -230,6 +232,7 @@ h.RegisterCheck(
Lottery{myname: "custom.lottery.check", probability:0.3},
gosundheit.InitialDelay(1*time.Second),
gosundheit.ExecutionPeriod(30*time.Second),
gosundheit.ExecutionTimeout(5*time.Second),
)
```

Expand All @@ -238,6 +241,7 @@ h.RegisterCheck(
but will not be concurrently executed.
1. Checks must complete within a reasonable time. If a check doesn't complete or gets hung,
the next check execution will be delayed. Use proper time outs.
1. Checks must respect the provided context. Specifically, a check must abort its execution, and return an error, if the context has been cancelled.
1. **A health-check name must be a metric name compatible string**
(i.e. no funky characters, and spaces allowed - just make it simple like `clicks-db-check`).
See here: https://help.datadoghq.com/hc/en-us/articles/203764705-What-are-valid-metric-names-
Expand Down
5 changes: 4 additions & 1 deletion check.go
@@ -1,11 +1,14 @@
package gosundheit

import "context"

// Check is the API for defining health checks.
// A valid check has a non empty Name() and a check (Execute()) function.
type Check interface {
// Name is the name of the check.
// Check names must be metric compatible.
Name() string
// Execute runs a single time check, and returns an error when the check fails, and an optional details object.
Execute() (details interface{}, err error)
// The function is expected to exit as soon as the provided Context is Done.
Execute(ctx context.Context) (details interface{}, err error)
}
15 changes: 13 additions & 2 deletions check_task.go
@@ -1,13 +1,15 @@
package gosundheit

import (
"context"
"time"
)

type checkTask struct {
stopChan chan bool
ticker *time.Ticker
check Check
timeout time.Duration
}

func (t *checkTask) stop() {
Expand All @@ -16,10 +18,19 @@ func (t *checkTask) stop() {
}
}

func (t *checkTask) execute() (details interface{}, duration time.Duration, err error) {
func (t *checkTask) execute(ctx context.Context) (details interface{}, duration time.Duration, err error) {
timeoutCtx, cancel := contextWithTimeout(ctx, t.timeout)
defer cancel()
startTime := time.Now()
details, err = t.check.Execute()
details, err = t.check.Execute(timeoutCtx)
duration = time.Since(startTime)

return
}

func contextWithTimeout(parent context.Context, t time.Duration) (context.Context, context.CancelFunc) {
if t <= 0 {
return context.WithCancel(parent)
}
return context.WithTimeout(parent, t)
}
11 changes: 7 additions & 4 deletions checks/custom.go
@@ -1,13 +1,16 @@
package checks

import gosundheit "github.com/AppsFlyer/go-sundheit"
import (
"context"
gosundheit "github.com/AppsFlyer/go-sundheit"
)

// CustomCheck is a simple Check implementation if all you need is a functional check
type CustomCheck struct {
// CheckName s the name of the check.
CheckName string
// CheckFunc is a function that runs a single time check, and returns an error when the check fails, and an optional details object.
CheckFunc func() (details interface{}, err error)
CheckFunc func(ctx context.Context) (details interface{}, err error)
}

var _ gosundheit.Check = (*CustomCheck)(nil)
Expand All @@ -19,10 +22,10 @@ func (check *CustomCheck) Name() string {
}

// Execute runs the given Checkfunc, and return it's output.
func (check *CustomCheck) Execute() (details interface{}, err error) {
func (check *CustomCheck) Execute(ctx context.Context) (details interface{}, err error) {
if check.CheckFunc == nil {
return "Unimplemented check", nil
}

return check.CheckFunc()
return check.CheckFunc(ctx)
}
7 changes: 4 additions & 3 deletions checks/custom_test.go
@@ -1,6 +1,7 @@
package checks

import (
"context"
"errors"
"testing"

Expand All @@ -18,17 +19,17 @@ func TestName(t *testing.T) {

func TestExecute(t *testing.T) {
chk := CustomCheck{}
details, err := chk.Execute()
details, err := chk.Execute(context.Background())
assert.Nil(t, err, "nil check func should execute and return nil error")
assert.Equal(t, "Unimplemented check", details, "nil check func should execute and return details")

const expectedDetails = "my.details"
expectedErr := errors.New("my.error")
chk.CheckFunc = func() (details interface{}, err error) {
chk.CheckFunc = func(ctx context.Context) (details interface{}, err error) {
return expectedDetails, expectedErr
}

details, err = chk.Execute()
details, err = chk.Execute(context.Background())
assert.Equal(t, expectedDetails, details)
assert.Equal(t, expectedErr, err)
}
20 changes: 8 additions & 12 deletions checks/dns.go
Expand Up @@ -4,30 +4,27 @@ import (
"context"
"fmt"
"net"
"time"

gosundheit "github.com/AppsFlyer/go-sundheit"
"github.com/pkg/errors"

gosundheit "github.com/AppsFlyer/go-sundheit"
)

// NewHostResolveCheck returns a gosundheit.Check that makes sure the provided host can resolve
// to at least `minRequiredResults` IP address within the specified timeout.
func NewHostResolveCheck(host string, timeout time.Duration, minRequiredResults int) gosundheit.Check {
return NewResolveCheck(NewHostLookup(nil), host, timeout, minRequiredResults)
// to at least `minRequiredResults` IP address within the timeout specified by the provided context..
func NewHostResolveCheck(host string, minRequiredResults int) gosundheit.Check {
return NewResolveCheck(NewHostLookup(nil), host, minRequiredResults)
}

// LookupFunc is a function that is used for looking up something (in DNS) and return the resolved results count, and a possible error
type LookupFunc func(ctx context.Context, lookFor string) (resolvedCount int, err error)

// NewResolveCheck returns a gosundheit.Check that makes sure the `resolveThis` arg can be resolved using the `lookupFn`
// to at least `minRequiredResults` result within the specified timeout.
func NewResolveCheck(lookupFn LookupFunc, resolveThis string, timeout time.Duration, minRequiredResults int) gosundheit.Check {
// to at least `minRequiredResults` result, within the timeout specified by the provided context.
func NewResolveCheck(lookupFn LookupFunc, resolveThis string, minRequiredResults int) gosundheit.Check {
return &CustomCheck{
CheckName: "resolve." + resolveThis,
CheckFunc: func() (details interface{}, err error) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()

CheckFunc: func(ctx context.Context) (details interface{}, err error) {
resolvedCount, err := lookupFn(ctx, resolveThis)
details = fmt.Sprintf("[%d] results were resolved", resolvedCount)
if err != nil {
Expand All @@ -36,7 +33,6 @@ func NewResolveCheck(lookupFn LookupFunc, resolveThis string, timeout time.Durat
if resolvedCount < minRequiredResults {
err = errors.Errorf("[%s] lookup returned %d results, but requires at least %d", resolveThis, resolvedCount, minRequiredResults)
}

return
},
}
Expand Down
24 changes: 14 additions & 10 deletions checks/dns_test.go
Expand Up @@ -11,31 +11,35 @@ import (
)

func TestNewHostResolveCheck(t *testing.T) {
check := NewHostResolveCheck("127.0.0.1", 10*time.Microsecond, 1)
check := NewHostResolveCheck("127.0.0.1", 1)

assert.Equal(t, "resolve.127.0.0.1", check.Name(), "check name")

details, err := check.Execute()
details, err := check.Execute(context.Background())
assert.NoError(t, err, "check execution should succeed")
assert.Equal(t, "[1] results were resolved", details)
}

func TestNewHostResolveCheck_noSuchHost(t *testing.T) {
check := NewHostResolveCheck("I-hope-there-is.no.such.host.com", 1*time.Second, 1)
check := NewHostResolveCheck("I-hope-there-is.no.such.host.com", 1)

assert.Equal(t, "resolve.I-hope-there-is.no.such.host.com", check.Name(), "check name")

details, err := check.Execute()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
details, err := check.Execute(ctx)

assert.Error(t, err, "check execution should fail")
assert.Contains(t, err.Error(), "no such host")
assert.Equal(t, "[0] results were resolved", details)
}

func TestNewHostResolveCheck_timeout(t *testing.T) {
check := NewHostResolveCheck("I-hope-there-is.no.such.host.com", 1, 1)
check := NewHostResolveCheck("I-hope-there-is.no.such.host.com", 1)

details, err := check.Execute()
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
details, err := check.Execute(ctx)

assert.Error(t, err, "check execution should fail")
assert.Contains(t, err.Error(), "i/o timeout")
Expand All @@ -48,18 +52,18 @@ const (
)

func TestNewResolveCheck_lookupError(t *testing.T) {
check := NewResolveCheck(creteMockLookupFunc(ExpectedCount, errors.New(ExpectedError)), "whatever", 1, 1)
check := NewResolveCheck(creteMockLookupFunc(ExpectedCount, errors.New(ExpectedError)), "whatever", 1)

assert.Equal(t, "resolve.whatever", check.Name(), "check name")
details, err := check.Execute()
details, err := check.Execute(context.Background())
assert.EqualErrorf(t, err, ExpectedError, "error message")
assert.Equal(t, fmt.Sprintf("[%d] results were resolved", ExpectedCount), details)
}

func TestNewResolveCheck_expectedCount(t *testing.T) {
check := NewResolveCheck(creteMockLookupFunc(0, nil), "whatever", 1, ExpectedCount)
check := NewResolveCheck(creteMockLookupFunc(0, nil), "whatever", ExpectedCount)

details, err := check.Execute()
details, err := check.Execute(context.Background())
assert.EqualErrorf(t, err, fmt.Sprintf("[whatever] lookup returned 0 results, but requires at least %d", ExpectedCount), "error message")
assert.Equal(t, "[0] results were resolved", details)
}
Expand Down
9 changes: 5 additions & 4 deletions checks/http.go
@@ -1,6 +1,7 @@
package checks

import (
"context"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -90,9 +91,9 @@ func (check *httpCheck) Name() string {
return check.config.CheckName
}

func (check *httpCheck) Execute() (details interface{}, err error) {
func (check *httpCheck) Execute(ctx context.Context) (details interface{}, err error) {
details = check.config.URL
resp, err := check.fetchURL()
resp, err := check.fetchURL(ctx)
if err != nil {
return details, err
}
Expand Down Expand Up @@ -120,8 +121,8 @@ func (check *httpCheck) Execute() (details interface{}, err error) {

// fetchURL executes the HTTP request to the target URL, and returns a `http.Response`, error.
// It is the callers responsibility to close the response body
func (check *httpCheck) fetchURL() (*http.Response, error) {
req, err := http.NewRequest(check.config.Method, check.config.URL, check.config.Body())
func (check *httpCheck) fetchURL(ctx context.Context) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, check.config.Method, check.config.URL, check.config.Body())
if err != nil {
return nil, errors.Errorf("unable to create check HTTP request: %v", err)
}
Expand Down

0 comments on commit 3ecbb7e

Please sign in to comment.