Skip to content

Commit

Permalink
Merge pull request #257 from alinrosca97/126-detailed-health-endpoint…
Browse files Browse the repository at this point in the history
…-with-json-output

detailed json health check endpoint
  • Loading branch information
threez committed Apr 13, 2021
2 parents 378d8c0 + d581ff8 commit 74ac2c4
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
1 change: 1 addition & 0 deletions http/router.go
Expand Up @@ -62,6 +62,7 @@ func Router() *mux.Router {

r.Handle("/health", servicehealthcheck.HealthHandler())
r.Handle("/health/check", servicehealthcheck.ReadableHealthHandler())
r.Handle("/health/check.json", servicehealthcheck.JSONHealthHandler())

// for debugging purposes (e.g. deadlock, ...)
p := r.PathPrefix("/debug/pprof").Subrouter()
Expand Down
7 changes: 7 additions & 0 deletions maintenance/health/servicehealthcheck/healthchecker.go
Expand Up @@ -200,3 +200,10 @@ func HealthHandler() http.Handler {
func ReadableHealthHandler() http.Handler {
return &readableHealthHandler{}
}

// JSONHealthHandler return health endpoint with all details about service health. This handler checks
// all health checks. The response body contains a JSON formatted array with every service (required or optional)
// and the detailed health checks about them.
func JSONHealthHandler() http.Handler {
return &jsonHealthHandler{}
}
104 changes: 104 additions & 0 deletions maintenance/health/servicehealthcheck/healthchecker_test.go
Expand Up @@ -5,6 +5,7 @@ package servicehealthcheck

import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -247,6 +248,109 @@ func TestHandlerReadableHealthCheck(t *testing.T) {
}
}

func TestHandlerJSONHealthCheck(t *testing.T) {
warn := &testHealthChecker{name: "WithWarning", healthCheckWarn: true}
err := &testHealthChecker{name: "WithErr", healthCheckErr: true}
initErr := &testHealthChecker{name: "WithInitErr", initErr: true}

testcases := []struct {
title string
req []*testHealthChecker
opt []*testHealthChecker
expCode int
expReq []jsonHealthHandler
}{
{
title: "Test health check json all required",
req: []*testHealthChecker{warn, err, initErr},
opt: []*testHealthChecker{},
expCode: http.StatusServiceUnavailable,
expReq: []jsonHealthHandler{
jsonHealthHandler{
Name: err.name,
Status: Err,
Required: true,
Error: "healthCheckErr",
},
jsonHealthHandler{
Name: initErr.name,
Status: Err,
Required: true,
Error: "initError",
},
jsonHealthHandler{
Name: warn.name,
Status: Ok,
Required: true,
Error: "",
},
},
},
{
title: "Test health check json some required, some optional",
req: []*testHealthChecker{warn, err},
opt: []*testHealthChecker{initErr},
expCode: http.StatusServiceUnavailable,
expReq: []jsonHealthHandler{
jsonHealthHandler{
Name: err.name,
Status: Err,
Required: true,
Error: "healthCheckErr",
},
jsonHealthHandler{
Name: initErr.name,
Status: Err,
Required: false,
Error: "initError",
},
jsonHealthHandler{
Name: warn.name,
Status: Ok,
Required: true,
Error: "",
},
},
},
}
for _, tc := range testcases {
t.Run(tc.title, func(t *testing.T) {
resetHealthChecks()

rec := httptest.NewRecorder()
for _, hc := range tc.req {
RegisterHealthCheck(hc.name, hc)
}
for _, hc := range tc.opt {
RegisterOptionalHealthCheck(hc, hc.name)
}
req := httptest.NewRequest("GET", "/health/check", nil)
JSONHealthHandler().ServeHTTP(rec, req)
resp := rec.Result()

require.Equal(t, tc.expCode, resp.StatusCode)

var data []jsonHealthHandler
err := json.NewDecoder(resp.Body).Decode(&data)
require.NoError(t, err)

sort.Slice(data, func(i, j int) bool {
return data[i].Name < data[j].Name
})
sort.Slice(tc.expReq, func(i, j int) bool {
return tc.expReq[i].Name < tc.expReq[j].Name
})

for i := range tc.expReq {
require.Equal(t, tc.expReq[i].Name, data[i].Name)
require.Equal(t, tc.expReq[i].Status, data[i].Status)
require.Equal(t, tc.expReq[i].Required, data[i].Required)
require.Equal(t, tc.expReq[i].Error, data[i].Error)
}
})
}
}

func testListHealthChecks(expected []string, checkResult []string, t *testing.T) {
// checkResult contains a empty string because of the splitting
require.Equal(t, len(expected), len(checkResult)-1, "The amount of health check results in the response body needs to be equal to the amount of expected health check results.")
Expand Down
62 changes: 62 additions & 0 deletions maintenance/health/servicehealthcheck/json_health_handler.go
@@ -0,0 +1,62 @@
package servicehealthcheck

import (
"encoding/json"
"net/http"

"github.com/pace/bricks/maintenance/log"
)

type jsonHealthHandler struct {
Name string `json: "name"`
Status HealthState `json: "status"`
Required bool `json: "required"`
Error string `json: "error"`
}

func (h *jsonHealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug("JSON HTTP handler")
s := log.Sink{Silent: true}
ctx := log.ContextWithSink(r.Context(), &s)

reqChecks := check(ctx, &requiredChecks)
optChecks := check(ctx, &optionalChecks)

checkResponse := make([]jsonHealthHandler, len(reqChecks)+len(optChecks))
index := 0

status := http.StatusOK
for name, res := range reqChecks {
scr := jsonHealthHandler{
Name: name,
Status: res.State,
Required: true,
Error: "",
}
if res.State == Err {
scr.Error = res.Msg
status = http.StatusServiceUnavailable
}
checkResponse[index] = scr
index++
}

for name, res := range optChecks {
scr := jsonHealthHandler{
Name: name,
Status: res.State,
Required: false,
Error: "",
}
if res.State == Err {
scr.Error = res.Msg
status = http.StatusServiceUnavailable
}
checkResponse[index] = scr
index++
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(checkResponse)
}

0 comments on commit 74ac2c4

Please sign in to comment.