Skip to content

Commit

Permalink
Minor housekeeping (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredpetersen authored Oct 14, 2021
1 parent 1213760 commit ef04235
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 167 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup Go environment
uses: actions/setup-go@v2
with:
go-version: '1.17'
- name: Checkout code
uses: actions/checkout@v2
- name: Install tool dependencies
run: make install
- name: Static code analysis
run: make check
- name: Validate formatting
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
- name: Build
run: make build
- name: Test & generate code coverage
run: make test
- name: Evaluate code coverage
run: |
coverage=$(go tool cover -func cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}')
if [[ $(bc -l <<< "$coverage > 90") -eq 1 ]]; then
echo "Code coverage: PASS"
exit 0
else
echo "Code coverage: FAIL"
exit 1
fi
20 changes: 0 additions & 20 deletions .github/workflows/ci.yaml

This file was deleted.

14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2021-10-14
### Added
- Godoc examples.
- Static code analysis with [staticcheck](https://staticcheck.io/).

### Changed
- `Monitor()` function on `Monitor` now takes variadic `Check` arguments, allowing you to pass multiple checks in at
once. This does not break backwards compatability.

### Fixed
- README example that didn't compile.

## [0.0.0] - 2021-10-13
### Added
- Initial development release
- Initial development release.
46 changes: 20 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# go-health
#### 🏥 Barebones, detailed health check library for Go
[![Build](https://github.com/jaredpetersen/go-health/actions/workflows/build.yaml/badge.svg)](https://github.com/jaredpetersen/go-health/actions/workflows/build.yaml)
[![Go Reference](https://pkg.go.dev/badge/github.com/jaredpetersen/go-health/health.svg)](https://pkg.go.dev/github.com/jaredpetersen/go-health/health)

go-health does away with the kitchen sink mentality of other health check libraries. You aren't getting a default HTTP
handler out of the box that is router dependent or has opinions about the shape or format of the health data being
published. You aren't getting pre-built health checks. But you do get a simple system for checking the health of
resources asynchronously with built-in caching and timeouts. Only what you absolutely need, and nothing else.

## Quickstart
Install the package:
```sh
go get github.com/jaredpetersen/go-health/health@latest
```

Example:
```go
// Create the health monitor that will be polling the resources.
healthMonitor := health.New()
Expand All @@ -15,40 +24,24 @@ ctx := context.Background()

// Create your health checks.
fooHealthCheckFunc := func(ctx context.Context) health.Status {
return health.Status{State: health.StateUp}
return health.Status{State: health.StateDown}
}
fooHealthCheck := health.NewCheck("foo", fooHealthCheckFunc)
fooHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, fooHealthCheck)

barHealthCheckFunc := func(ctx context.Context) health.Status {
statusDown := health.Status{State: health.StateDown}

// Create a HTTP request that terminates when the context is terminated.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)
if err != nil {
return statusDown
}

// Execute the HTTP request.
client := http.Client{}
res, err := client.Do(req)
if err != nil {
return statusDown
}

if res.StatusCode == http.StatusOK {
return health.Status{State: health.StateUp}
} else {
return statusDown
}
return health.Status{State: health.StateUp}
}
barHealthCheck := health.NewCheck("bar", barCheckFunc)
barHealthCheck.TTL = time.Second * 5
barHealthCheck := health.NewCheck("bar", barHealthCheckFunc)
barHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, barHealthCheck)

// Wait for goroutines to kick off
time.Sleep(time.Millisecond * 100)

// Retrieve the most recent cached result for all of the checks.
healthStatus := healthMonitor.Check()
healthMonitor.Check()
```

## Asynchronous Checking and Caching
Expand Down Expand Up @@ -81,13 +74,14 @@ The return type of the health check function supports adding arbitrary informati
information like active database connections, response time for an HTTP request, etc.

```go
type CustomHTTPStatusDetails struct {
type HTTPHealthCheckDetails struct {
ResponseTime time.Duration
}
```

```go
return health.Status{
State: health.StateUp,
Details: CustomHTTPStatusDetails{ResponseTime: time.Millisecond * 352},
Details: HTTPHealthCheckDetails{ResponseTime: responseTime},
}
```
97 changes: 97 additions & 0 deletions health/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package health_test

import (
"context"
"net/http"
"time"

"github.com/jaredpetersen/go-health/health"
)

func Example() {
// Create the health monitor that will be polling the resources.
healthMonitor := health.New()

// Prepare the context -- this can be used to stop async monitoring.
ctx := context.Background()

// Create your health checks.
fooHealthCheckFunc := func(ctx context.Context) health.Status {
return health.Status{State: health.StateDown}
}
fooHealthCheck := health.NewCheck("foo", fooHealthCheckFunc)
fooHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, fooHealthCheck)

barHealthCheckFunc := func(ctx context.Context) health.Status {
return health.Status{State: health.StateUp}
}
barHealthCheck := health.NewCheck("bar", barHealthCheckFunc)
barHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, barHealthCheck)

// Wait for goroutines to kick off
time.Sleep(time.Millisecond * 100)

// Retrieve the most recent cached result for all of the checks.
healthMonitor.Check()
}

func Example_http() {
// Create the health monitor that will be polling the resources.
healthMonitor := health.New()

// Prepare the context -- this can be used to stop async monitoring.
ctx := context.Background()

// Set up a generic health checker, though anything that implements the check function will do.
httpClient := http.Client{}
type HTTPHealthCheckDetails struct {
ResponseTime time.Duration
}
httpHealthCheckFunc := func(url string) health.CheckFunc {
statusDown := health.Status{State: health.StateDown}

return func(ctx context.Context) health.Status {
// Create a HTTP request that terminates when the context is terminated.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return statusDown
}

// Execute the HTTP request
requestStart := time.Now()
res, err := httpClient.Do(req)
responseTime := time.Since(requestStart)
if err != nil {
return statusDown
}

if res.StatusCode == http.StatusOK {
return health.Status{
State: health.StateUp,
Details: HTTPHealthCheckDetails{ResponseTime: responseTime},
}
} else {
return statusDown
}
}
}

// Create your health checks.
exampleHealthCheckFunc := httpHealthCheckFunc("http://example.com")
exampleHealthCheck := health.NewCheck("example", exampleHealthCheckFunc)
exampleHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, exampleHealthCheck)

godevHealthCheckFunc := httpHealthCheckFunc("https://go.dev")
godevHealthCheck := health.NewCheck("godev", godevHealthCheckFunc)
godevHealthCheck.Timeout = time.Second * 2
healthMonitor.Monitor(ctx, godevHealthCheck)

// Wait for goroutines to kick off
time.Sleep(time.Second * 2)

// Retrieve the most recent cached result for all of the checks.
healthMonitor.Check()
}
76 changes: 40 additions & 36 deletions health/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,18 @@ type Status struct {
Details interface{}
}

// CheckFunc is a function used to determine resource health.
type CheckFunc func(ctx context.Context) Status

// Check represents a resource to be checked. The check function is used to determine resource health and is executed
// on a cadence defined by the configured TTL.
type Check struct {
// Name of the check. Must be unique.
Name string
// Func is the resource health check function to be executed when determining health. This will be executed on a
// cadence as defined by the configured TTL. It is your responsibility to ensure that this function respects the
// provided context so that the logic may be terminated early. The provided context will be given a deadline if the
// check is configured with a timeout.
Func func(ctx context.Context) Status
// Func is used to determine resource health. This will be executed on a cadence as defined by the configured TTL.
// It is your responsibility to ensure that this function respects the provided context so that the logic may be
// terminated early. The provided context will be given a deadline if the check is configured with a timeout.
Func CheckFunc
// TTL is the time that should be waited on between executions of the health check function.
TTL time.Duration
// Timeout is the max time that the check function may execute in before the provided context communicates
Expand All @@ -70,7 +72,7 @@ type Check struct {
//
// Timeout is left at its zero-value, meaning that there is no deadline for completion. It is recommended that you
// configure a timeout yourself but this is not required.
func NewCheck(name string, checkFunc func(ctx context.Context) Status) Check {
func NewCheck(name string, checkFunc CheckFunc) Check {
return Check{
Name: name,
Func: checkFunc,
Expand Down Expand Up @@ -101,38 +103,40 @@ func (mtr *Monitor) setCheckStatus(checkName string, checkStatus CheckStatus) {
mtr.mtx.Unlock()
}

// Monitor starts a goroutine the executes the checks' check function and caches the result. This goroutine will wait
// between polls as defined by check's TTL to avoid spamming the resource being evaluated. If a timeout is set on the
// check, the context provided to Monitor will be wrapped in a deadline context and provided to the check function to
// facilitate early termination.
func (mtr *Monitor) Monitor(ctx context.Context, check Check) {
// Initialize the cache as StateDown
initialStatus := CheckStatus{
Status: Status{
State: StateDown,
},
}
mtr.setCheckStatus(check.Name, initialStatus)

// Start polling the check resource asynchronously
go func() {
for {
select {
case <-ctx.Done():
return
default:
var checkStatus CheckStatus
if check.Timeout > 0 {
checkStatus = executeCheckWithTimeout(ctx, check)
} else {
checkStatus = executeCheck(ctx, check)
// Monitor starts a goroutine for each check the executes the check's function and caches the result. This goroutine
// will wait between polls as defined by check's TTL to avoid spamming the resource being evaluated. If a timeout is
// set on the check, the context provided to Monitor will be wrapped in a deadline context and provided to the check
// function to facilitate early termination.
func (mtr *Monitor) Monitor(ctx context.Context, checks ...Check) {
for _, check := range checks {
// Initialize the cache as StateDown
initialStatus := CheckStatus{
Status: Status{
State: StateDown,
},
}
mtr.setCheckStatus(check.Name, initialStatus)

// Start polling the check resource asynchronously
go func(check Check) {
for {
select {
case <-ctx.Done():
return
default:
var checkStatus CheckStatus
if check.Timeout > 0 {
checkStatus = executeCheckWithTimeout(ctx, check)
} else {
checkStatus = executeCheck(ctx, check)
}

mtr.setCheckStatus(check.Name, checkStatus)
time.Sleep(check.TTL)
}

mtr.setCheckStatus(check.Name, checkStatus)
time.Sleep(check.TTL)
}
}
}()
}(check)
}
}

// Check returns the latest cached status for all of the configured checks.
Expand Down
Loading

0 comments on commit ef04235

Please sign in to comment.