Skip to content

Commit

Permalink
Release 0.6.0.
Browse files Browse the repository at this point in the history
Content for release 0.6.0
  • Loading branch information
alexliesenfeld authored Aug 9, 2021
2 parents 1a4ab30 + 3435112 commit ca4ca11
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 24 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## 0.6.0
### Breaking Changes
- A [ResultWriter](https://pkg.go.dev/github.com/alexliesenfeld/health#ResultWriter) must now additionally write the
status code into the [http.ResponseWriter](https://pkg.go.dev/net/http#ResponseWriter). This is necessary due to
ordering constraints when writing into a [http.ResponseWriter](https://pkg.go.dev/net/http#ResponseWriter)
(see https://github.com/alexliesenfeld/health/issues/9).

### Improvements:
- [Stopping the Checker](https://pkg.go.dev/github.com/alexliesenfeld/health#Checker) does not wait
[initial delay of periodic checks](https://pkg.go.dev/github.com/alexliesenfeld/health#WithPeriodicCheck)
has passed anymore. [Checker.Stop](https://pkg.go.dev/github.com/alexliesenfeld/health#Checker) stops
the [Checker](https://pkg.go.dev/github.com/alexliesenfeld/health#Checker) immediately, but waits until all currently
running check functions have completed.
- The [health check http.Handler](https://pkg.go.dev/github.com/alexliesenfeld/health#NewHandler) was patched to not
include an empty `checks` map in the JSON response. In case no check functions are defined, the JSON response will
therefore not be `{ "status": "up", "checks" : {} }` anymore but only `{ "status": "up" }`.
- A Kubernetes liveness and readiness checks example was added (see `examples/kubernetes`).

## 0.5.1
- Many documentation improvements

## 0.5.0

- BREAKING CHANGE: Changed function signature of middleware functions.
Expand Down
30 changes: 18 additions & 12 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,31 +256,27 @@ func (ck *defaultChecker) startPeriodicChecks() {
// Start periodic checks
for _, check := range ck.cfg.checks {
if isPeriodicCheck(check) {
var wg *sync.WaitGroup
endChan := make(chan *sync.WaitGroup, 1)
checkState := ck.state.CheckState[check.Name]
ck.endChans = append(ck.endChans, endChan)
go func(check Check, cfg checkerConfig, state CheckState) {
defer close(endChan)
if check.initialDelay > 0 {
time.Sleep(check.initialDelay)
if waitForStopSignal(check.initialDelay, endChan) {
return
}
}
loop:
for {
withCheckContext(context.Background(), &check, func(ctx context.Context) {
ctx, state = executeCheck(ctx, &cfg, &check, state)
ck.mtx.Lock()
ck.updateState(ctx, checkResult{check.Name, state})
ck.mtx.Unlock()
})
select {
case <-time.After(check.updateInterval):
case wg = <-endChan:
break loop
if waitForStopSignal(check.updateInterval, endChan) {
return
}
}
close(endChan)
wg.Done()
}(*check, ck.cfg, checkState)
}(*check, ck.cfg, ck.state.CheckState[check.Name])
}
}
}
Expand All @@ -302,7 +298,7 @@ func (ck *defaultChecker) mapStateToCheckerResult() CheckerResult {
var status = ck.state.Status
var checkResults *map[string]CheckResult

if !ck.cfg.detailsDisabled {
if len(ck.cfg.checks) > 0 && !ck.cfg.detailsDisabled {
checkResults = &map[string]CheckResult{}
for _, c := range ck.cfg.checks {
checkState := ck.state.CheckState[c.Name]
Expand All @@ -325,6 +321,16 @@ func isPeriodicCheck(check *Check) bool {
return check.updateInterval > 0
}

func waitForStopSignal(waitTime time.Duration, interruptChannel <-chan *sync.WaitGroup) bool {
select {
case <-time.After(waitTime):
return false
case wg := <-interruptChannel:
wg.Done()
return true
}
}

func withCheckContext(ctx context.Context, check *Check, f func(checkCtx context.Context)) {
cancel := func() {}
if check.Timeout > 0 {
Expand Down
5 changes: 4 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ type (
)

// NewChecker creates a new Checker. The provided options will be
// used to modify its configuration.
// used to modify its configuration. If the Checker was not yet started
// (see Checker.IsStarted), it will be started automatically
// (see Checker.Start). You can disable this autostart by
// adding the WithDisabledAutostart configuration option.
func NewChecker(options ...CheckerOption) Checker {
cfg := checkerConfig{
cacheTTL: 1 * time.Second,
Expand Down
22 changes: 22 additions & 0 deletions examples/kubernetes/example-pod-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file contains a Kubernetes pod configuration example with a readiness and liveliness check.
# It can be used to complement the health check implementation from file "main.go" in the same directory.
apiVersion: v1
kind: Pod
metadata:
name: kubernetes-checks-example
spec:
containers:
- name: my-app
image: my-app-registry/my-app-image

livenessProbe:
httpGet:
path: /live
port: 3000
periodSeconds: 3

readinessProbe:
httpGet:
path: /ready
port: 3000
periodSeconds: 3
80 changes: 80 additions & 0 deletions examples/kubernetes/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"context"
"database/sql"
"fmt"
"github.com/alexliesenfeld/health"
_ "github.com/mattn/go-sqlite3"
"log"
"net/http"
"time"
)

// This is a an example configuration for Kubernetes liveness and readiness checks (for more info, please refer to
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
// Please note that Kubernetes readiness and especially liveness checks need to be designed with care to not cause
// any unintended behaviour (such as unexpected pod restarts, cascading failures, etc.). Please refer to the following
// articles for guidance:
// - https://www.innoq.com/en/blog/kubernetes-probes/
// - https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/
// - https://srcco.de/posts/kubernetes-liveness-probes-are-dangerous.html
// Attention: Please see file `example-pod-config.yaml` in the same directory for an example configuration
// that you can use to complement this check implementation example.
func main() {
db, _ := sql.Open("sqlite3", "simple.sqlite")
defer db.Close()

// Create a new Checker for our readiness check.
readinessChecker := health.NewChecker(

// Configure a global timeout that will be applied to all check functions.
health.WithTimeout(10*time.Second),

// A check configuration to see if our database connection is up.
// Be wary though that this should be a "service private" database instance.
// If many of your services use the same database instance, the readiness checks
// of all of these services will start failing on every small database hick-up.
// This is most likely not what you want. For guidance, please refer to the links
// listed in the main function documentation above.
health.WithCheck(health.Check{
Name: "database", // A unique check name.
Check: db.PingContext,
}),

// The following check will be executed periodically every 15 seconds
// started with an initial delay of 3 seconds. The check function will NOT
// be executed for each HTTP request.
health.WithPeriodicCheck(15*time.Second, 3*time.Second, health.Check{
Name: "search",
// The check function checks the health of a component. If an error is
// returned, the component is considered unavailable ("down").
// The context contains a deadline according to the configuration of
// the Checker (global and .
Check: func(ctx context.Context) error {
return fmt.Errorf("this makes the check fail")
},
}),

// Set a status listener that will be invoked when the health status changes.
// More powerful hooks are also available (see docs). For guidance, please refer to the links
// listed in the main function documentation above.
health.WithStatusListener(func(ctx context.Context, state health.CheckerState) {
log.Println(fmt.Sprintf("health status changed to %s", state.Status))
}),
)

// Liveness check should mostly contain checks that identify if the service is locked up or in a state that it
// cannot recover from (deadlocks, etc.). In most cases it should just respond with 200 OK to avoid unexpected
// restarts.
livenessChecker := health.NewChecker()

// Create a new health check http.Handler that returns the health status
// serialized as a JSON string. You can pass pass further configuration
// options to NewHandler to modify default configuration.
http.Handle("/live", health.NewHandler(livenessChecker))
http.Handle("/ready", health.NewHandler(readinessChecker))

// Start the HTTP server
log.Fatalln(http.ListenAndServe(":3000", nil))
}
24 changes: 13 additions & 11 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ type (
ResultWriter interface {
// Write writes a CheckerResult into a http.ResponseWriter in a format
// that the ResultWriter supports (such as XML, JSON, etc.).
Write(result *CheckerResult, w http.ResponseWriter, r *http.Request) error
// A ResultWriter is expected to write at least the following information into the http.ResponseWriter:
// (1) A MIME type header (e.g., "Content-Type" : "application/json"),
// (2) the HTTP status code that is passed in parameter statusCode (this is necessary due to ordering constraints
// when writing into a http.ResponseWriter (see https://github.com/alexliesenfeld/health/issues/9), and
// (3) the response body in the format that the ResultWriter supports.
Write(result *CheckerResult, statusCode int, w http.ResponseWriter, r *http.Request) error
}

// JSONResultWriter writes a CheckerResult in JSON format into an
Expand All @@ -42,14 +47,15 @@ type (
)

// Write implements ResultWriter.Write.
func (r *JSONResultWriter) Write(result *CheckerResult, w http.ResponseWriter, req *http.Request) error {
func (rw *JSONResultWriter) Write(result *CheckerResult, statusCode int, w http.ResponseWriter, r *http.Request) error {
jsonResp, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("cannot marshal response: %w", err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(jsonResp)
return nil
w.WriteHeader(statusCode)
_, err = w.Write(jsonResp)
return err
}

// NewJSONResultWriter creates a new instance of a JSONResultWriter.
Expand All @@ -58,10 +64,6 @@ func NewJSONResultWriter() *JSONResultWriter {
}

// NewHandler creates a new health check http.Handler.
// If the Checker was not yet started (see Checker.IsStarted),
// it will be started automatically (see Checker.Start).
// You can disable this autostart by adding the WithDisabledAutostart
// configuration option.
func NewHandler(checker Checker, options ...HandlerOption) http.HandlerFunc {
cfg := createConfig(options)
return func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -72,8 +74,8 @@ func NewHandler(checker Checker, options ...HandlerOption) http.HandlerFunc {

// Write HTTP response
disableResponseCache(w)
w.WriteHeader(mapHTTPStatus(result.Status, cfg.statusCodeUp, cfg.statusCodeDown))
cfg.resultWriter.Write(&result, w, r)
statusCode := mapHTTPStatusCode(result.Status, cfg.statusCodeUp, cfg.statusCodeDown)
cfg.resultWriter.Write(&result, statusCode, w, r)
}
}

Expand All @@ -86,7 +88,7 @@ func disableResponseCache(w http.ResponseWriter) {
w.Header().Set("Expires", "-1")
}

func mapHTTPStatus(status AvailabilityStatus, statusCodeUp int, statusCodeDown int) int {
func mapHTTPStatusCode(status AvailabilityStatus, statusCodeUp int, statusCodeDown int) int {
if status == StatusDown || status == StatusUnknown {
return statusCodeDown
}
Expand Down
15 changes: 15 additions & 0 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,18 @@ func TestHandlerIfAuthFailsThenReturnNoDetails(t *testing.T) {
}
doTestHandler(t, http.StatusNoContent, http.StatusTeapot, status, http.StatusTeapot)
}

func TestWhenChecksEmptyThenHandlerResultContainNoChecksMap(t *testing.T) {
// Arrange
r := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()

// Act
NewHandler(NewChecker()).ServeHTTP(w, r)

// Assert
if w.Body.String() != "{\"status\":\"up\"}" {
t.Errorf("response does not contain the expected result")
}

}

0 comments on commit ca4ca11

Please sign in to comment.