From 8903278b29f1dc38f0e64ef294febe0dcaff3f2a Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sat, 11 Jul 2020 13:24:58 +0200 Subject: [PATCH] Add healthcheck package --- pkg/healthcheck/check.go | 118 ++++++++++++++++++++++++++++++++ pkg/healthcheck/check_test.go | 68 ++++++++++++++++++ pkg/healthcheck/http_handler.go | 16 +++++ 3 files changed, 202 insertions(+) create mode 100644 pkg/healthcheck/check.go create mode 100644 pkg/healthcheck/check_test.go create mode 100644 pkg/healthcheck/http_handler.go diff --git a/pkg/healthcheck/check.go b/pkg/healthcheck/check.go new file mode 100644 index 000000000..4cd9d9b5a --- /dev/null +++ b/pkg/healthcheck/check.go @@ -0,0 +1,118 @@ +package healthcheck + +import ( + "bytes" + "context" + "fmt" + "io" + "strings" +) + +type ( + checkFn func(ctx context.Context) error + + Meta struct { + Label string + Description string + } + + check struct { + fn checkFn + *Meta + } + + result struct { + err error + *Meta + } + + results []*result + checks struct { + cc []*check + } +) + +var ( + defaults *checks +) + +func init() { + defaults = New() +} + +func Defaults() *checks { + return defaults +} + +func New() *checks { + return &checks{cc: []*check{}} +} + +// Add appends new check +func (c *checks) Add(fn checkFn, label string, description ...string) { + c.cc = append(c.cc, &check{fn, &Meta{Label: label, Description: strings.Join(description, "")}}) +} + +func (c checks) Run(ctx context.Context) results { + var rr = make([]*result, len(c.cc)) + + for i, c := range c.cc { + rr[i] = &result{c.fn(ctx), c.Meta} + } + + return rr +} + +func (rr results) Healthy() bool { + for _, c := range rr { + if c.err != nil { + return false + } + } + + return true +} + +func (rr results) String() string { + buf := &bytes.Buffer{} + + rr.WriteTo(buf) + + return buf.String() +} + +func (rr results) WriteTo(w io.Writer) { + var ( + p = func(f string, aa ...interface{}) { + _, _ = fmt.Fprintf(w, f, aa...) + } + ) + + for _, r := range rr { + if r.IsHealthy() { + p("PASS") + } else { + p("FAIL") + } + + p(" %s", r.Label) + + if !r.IsHealthy() { + p(": %v", r.Error()) + } + + p("\n") + } +} + +func (r *result) IsHealthy() bool { + return r != nil && r.err == nil +} + +func (r *result) Error() string { + if r == nil || r.err == nil { + return "" + } + + return r.err.Error() +} diff --git a/pkg/healthcheck/check_test.go b/pkg/healthcheck/check_test.go new file mode 100644 index 000000000..ab133dc98 --- /dev/null +++ b/pkg/healthcheck/check_test.go @@ -0,0 +1,68 @@ +package healthcheck + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Healthy(t *testing.T) { + tests := []struct { + name string + checks []*check + healthy bool + string string + }{ + { + "should be healthy with handle for stringer output", + []*check{{func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}}}, + true, + "PASS check01\n", + }, + { + "should handle multiple healthy checks", + []*check{ + {func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}}, + {func(ctx context.Context) error { return nil }, &Meta{Label: "check02"}}, + }, + true, + "PASS check01\nPASS check02\n", + }, + { + "should handle healthy and unhealthy checks", + []*check{ + {func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}}, + {func(ctx context.Context) error { return fmt.Errorf("x") }, &Meta{Label: "check02"}}, + {func(ctx context.Context) error { return nil }, &Meta{Label: "check03"}}, + }, + false, + "PASS check01\nFAIL check02: x\nPASS check03\n", + }, + { + "should handle labels", + []*check{ + {func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}}, + {func(ctx context.Context) error { return nil }, &Meta{Label: "Pretty check"}}, + }, + true, + "PASS check01\nPASS Pretty check\n", + }, + { + "should handle empty check list", + []*check{}, + true, + "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + r := (&checks{cc: tt.checks}).Run(context.Background()) + a.Equal(tt.healthy, r.Healthy(), "healthy result failed") + a.Equal(tt.string, r.String(), "stringer output match failed") + }) + } +} diff --git a/pkg/healthcheck/http_handler.go b/pkg/healthcheck/http_handler.go new file mode 100644 index 000000000..d1675ca47 --- /dev/null +++ b/pkg/healthcheck/http_handler.go @@ -0,0 +1,16 @@ +package healthcheck + +import "net/http" + +func HttpHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + results := Defaults().Run(r.Context()) + if results.Healthy() { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + + results.WriteTo(w) + } +}