Skip to content

Commit

Permalink
Merge pull request #41 from hellofresh/feature/expose-check
Browse files Browse the repository at this point in the history
EES-3610 Expose method to run health checks w/out HTTP handler
  • Loading branch information
vgarvardt authored Jul 23, 2020
2 parents 9ae2f03 + 47627b0 commit 3b00ca4
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 47 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

The library exports `Handler` and `HandlerFunc` functions which are fully compatible with `net/http`.

Additionally, library exports `Measure` function that returns summary status for all the registered health checks, so it can be used in non-HTTP environments.

### Handler

```go
Expand Down
74 changes: 43 additions & 31 deletions health.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ var (
checkMap = make(map[string]Config)
)

// Status type represents health status
type Status string

// Possible health statuses
const (
statusOK = "OK"
statusPartiallyAvailable = "Partially Available"
statusUnavailable = "Unavailable"
failureTimeout = "Timeout during health check"
StatusOK Status = "OK"
StatusPartiallyAvailable Status = "Partially Available"
StatusUnavailable Status = "Unavailable"
StatusTimeout Status = "Timeout during health check"
)

type (
Expand All @@ -41,7 +45,7 @@ type (
// Check represents the health check response.
Check struct {
// Status is the check status.
Status string `json:"status"`
Status Status `json:"status"`
// Timestamp is the time in which the check occurred.
Timestamp time.Time `json:"timestamp"`
// Failures holds the failed checks along with their messages.
Expand Down Expand Up @@ -100,10 +104,30 @@ func Handler() http.Handler {

// HandlerFunc is the HTTP handler function.
func HandlerFunc(w http.ResponseWriter, r *http.Request) {
c := Measure()

w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(c)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

code := http.StatusOK
if c.Status == StatusUnavailable {
code = http.StatusServiceUnavailable
}
w.WriteHeader(code)
w.Write(data)
}

// Measure runs all the registered health checks and returns summary status
func Measure() Check {
mu.Lock()
defer mu.Unlock()

status := statusOK
status := StatusOK
total := len(checkMap)
failures := make(map[string]string)
resChan := make(chan checkResponse, total)
Expand All @@ -113,12 +137,14 @@ func HandlerFunc(w http.ResponseWriter, r *http.Request) {

go func() {
defer close(resChan)

wg.Wait()
}()

for _, c := range checkMap {
go func(c Config) {
defer wg.Done()

select {
case resChan <- checkResponse{c.Name, c.SkipOnErr, c.Check()}:
default:
Expand All @@ -129,34 +155,20 @@ func HandlerFunc(w http.ResponseWriter, r *http.Request) {
for {
select {
case <-time.After(c.Timeout):
failures[c.Name] = failureTimeout
setStatus(&status, c.SkipOnErr)
failures[c.Name] = string(StatusTimeout)
status = getAvailability(status, c.SkipOnErr)
break loop
case res := <-resChan:
if res.err != nil {
failures[res.name] = res.err.Error()
setStatus(&status, res.skipOnErr)
status = getAvailability(status, res.skipOnErr)
}
break loop
}
}
}

w.Header().Set("Content-Type", "application/json")
c := newCheck(status, failures)
data, err := json.Marshal(c)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

code := http.StatusOK
if status == statusUnavailable {
code = http.StatusServiceUnavailable
}
w.WriteHeader(code)
w.Write(data)
return newCheck(status, failures)
}

// Reset unregisters all previously set check configs
Expand All @@ -167,9 +179,9 @@ func Reset() {
checkMap = make(map[string]Config)
}

func newCheck(status string, failures map[string]string) Check {
func newCheck(s Status, failures map[string]string) Check {
return Check{
Status: status,
Status: s,
Timestamp: time.Now(),
Failures: failures,
System: newSystemMetrics(),
Expand All @@ -189,10 +201,10 @@ func newSystemMetrics() System {
}
}

func setStatus(status *string, skipOnErr bool) {
if skipOnErr && *status != statusUnavailable {
*status = statusPartiallyAvailable
} else {
*status = statusUnavailable
func getAvailability(s Status, skipOnErr bool) Status {
if skipOnErr && s != StatusUnavailable {
return StatusPartiallyAvailable
}

return StatusUnavailable
}
29 changes: 13 additions & 16 deletions health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,12 @@ func TestRegisterWithNoName(t *testing.T) {
return nil
},
})
if err == nil {
t.Error("health check registration with empty name should return an error, but did not get one")
}
require.Error(t, err, "health check registration with empty name should return an error")
}

func TestDoubleRegister(t *testing.T) {
Reset()
if len(checkMap) != 0 {
t.Errorf("checks lenght differes from zero: got %d", len(checkMap))
}
assert.Len(t, checkMap, 0)

healthCheckName := "health-check"

Expand Down Expand Up @@ -63,22 +59,22 @@ func TestHealthHandler(t *testing.T) {

res := httptest.NewRecorder()
req, err := http.NewRequest("GET", "http://localhost/status", nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

Register(Config{
err = Register(Config{
Name: "rabbitmq",
SkipOnErr: true,
Check: func() error { return errors.New(checkErr) },
})
require.NoError(t, err)

Register(Config{
err = Register(Config{
Name: "mongodb",
Check: func() error { return nil },
})
require.NoError(t, err)

Register(Config{
err = Register(Config{
Name: "snail-service",
SkipOnErr: true,
Timeout: time.Second * 1,
Expand All @@ -87,8 +83,9 @@ func TestHealthHandler(t *testing.T) {
return nil
},
})
require.NoError(t, err)

h := http.Handler(Handler())
h := Handler()
h.ServeHTTP(res, req)

assert.Equal(t, http.StatusOK, res.Code, "status handler returned wrong status code")
Expand All @@ -97,7 +94,7 @@ func TestHealthHandler(t *testing.T) {
err = json.NewDecoder(res.Body).Decode(&body)
require.NoError(t, err)

assert.Equal(t, statusPartiallyAvailable, body["status"], "body returned wrong status")
assert.Equal(t, string(StatusPartiallyAvailable), body["status"], "body returned wrong status")

failure, ok := body["failures"]
assert.True(t, ok, "body returned nil failures field")
Expand All @@ -106,8 +103,8 @@ func TestHealthHandler(t *testing.T) {
assert.True(t, ok, "body returned nil failures.rabbitmq field")

assert.Equal(t, checkErr, f["rabbitmq"], "body returned wrong status for rabbitmq")
assert.Equal(t, failureTimeout, f["snail-service"], "body returned wrong status for snail-service")
assert.Equal(t, string(StatusTimeout), f["snail-service"], "body returned wrong status for snail-service")

Reset()
assert.Len(t, checkMap, 0, "checks length diffres from zero")
assert.Len(t, checkMap, 0, "checks length differs from zero")
}

0 comments on commit 3b00ca4

Please sign in to comment.