239 lines
6.0 KiB
Go
239 lines
6.0 KiB
Go
// Package check standardizes /health and /ready endpoints.
|
|
// This allows you to easily know when your server is ready and healthy.
|
|
package check
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// Status string to indicate the overall status of the check.
|
|
type Status string
|
|
|
|
const (
|
|
// StatusFail indicates a specific check has failed.
|
|
StatusFail Status = "fail"
|
|
// StatusPass indicates a specific check has passed.
|
|
StatusPass Status = "pass"
|
|
|
|
// DefaultCheckName is the name of the default checker.
|
|
DefaultCheckName = "internal"
|
|
)
|
|
|
|
// Check wraps a map of service names to status checkers.
|
|
type Check struct {
|
|
healthChecks []Checker
|
|
readyChecks []Checker
|
|
healthOverride override
|
|
readyOverride override
|
|
|
|
passthroughHandler http.Handler
|
|
}
|
|
|
|
// Checker indicates a service whose health can be checked.
|
|
type Checker interface {
|
|
Check(ctx context.Context) Response
|
|
}
|
|
|
|
// NewCheck returns a Health with a default checker.
|
|
func NewCheck() *Check {
|
|
ch := &Check{}
|
|
ch.healthOverride.disable()
|
|
ch.readyOverride.disable()
|
|
return ch
|
|
}
|
|
|
|
// AddHealthCheck adds the check to the list of ready checks.
|
|
// If c is a NamedChecker, the name will be added.
|
|
func (c *Check) AddHealthCheck(check Checker) {
|
|
if nc, ok := check.(NamedChecker); ok {
|
|
c.healthChecks = append(c.healthChecks, Named(nc.CheckName(), nc))
|
|
} else {
|
|
c.healthChecks = append(c.healthChecks, check)
|
|
}
|
|
}
|
|
|
|
// AddReadyCheck adds the check to the list of ready checks.
|
|
// If c is a NamedChecker, the name will be added.
|
|
func (c *Check) AddReadyCheck(check Checker) {
|
|
if nc, ok := check.(NamedChecker); ok {
|
|
c.readyChecks = append(c.readyChecks, Named(nc.CheckName(), nc))
|
|
} else {
|
|
c.readyChecks = append(c.readyChecks, check)
|
|
}
|
|
}
|
|
|
|
// CheckHealth evaluates c's set of health checks and returns a populated Response.
|
|
func (c *Check) CheckHealth(ctx context.Context) Response {
|
|
response := Response{
|
|
Name: "Health",
|
|
Status: StatusPass,
|
|
Checks: make(Responses, len(c.healthChecks)),
|
|
}
|
|
|
|
status, overriding := c.healthOverride.get()
|
|
if overriding {
|
|
response.Status = status
|
|
overrideResponse := Response{
|
|
Name: "manual-override",
|
|
Message: "health manually overridden",
|
|
}
|
|
response.Checks = append(response.Checks, overrideResponse)
|
|
}
|
|
for i, ch := range c.healthChecks {
|
|
resp := ch.Check(ctx)
|
|
if resp.Status != StatusPass && !overriding {
|
|
response.Status = resp.Status
|
|
}
|
|
response.Checks[i] = resp
|
|
}
|
|
sort.Sort(response.Checks)
|
|
return response
|
|
}
|
|
|
|
// CheckReady evaluates c's set of ready checks and returns a populated Response.
|
|
func (c *Check) CheckReady(ctx context.Context) Response {
|
|
response := Response{
|
|
Name: "Ready",
|
|
Status: StatusPass,
|
|
Checks: make(Responses, len(c.readyChecks)),
|
|
}
|
|
|
|
status, overriding := c.readyOverride.get()
|
|
if overriding {
|
|
response.Status = status
|
|
overrideResponse := Response{
|
|
Name: "manual-override",
|
|
Message: "ready manually overridden",
|
|
}
|
|
response.Checks = append(response.Checks, overrideResponse)
|
|
}
|
|
for i, c := range c.readyChecks {
|
|
resp := c.Check(ctx)
|
|
if resp.Status != StatusPass && !overriding {
|
|
response.Status = resp.Status
|
|
}
|
|
response.Checks[i] = resp
|
|
}
|
|
sort.Sort(response.Checks)
|
|
return response
|
|
}
|
|
|
|
// SetPassthrough allows you to set a handler to use if the request is not a ready or health check.
|
|
// This can be useful if you intend to use this as a middleware.
|
|
func (c *Check) SetPassthrough(h http.Handler) {
|
|
c.passthroughHandler = h
|
|
}
|
|
|
|
// ServeHTTP serves /ready and /health requests with the respective checks.
|
|
func (c *Check) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
const (
|
|
pathReady = "/ready"
|
|
pathHealth = "/health"
|
|
queryForce = "force"
|
|
)
|
|
|
|
path := r.URL.Path
|
|
|
|
// Allow requests not intended for checks to pass through.
|
|
if path != pathReady && path != pathHealth {
|
|
if c.passthroughHandler != nil {
|
|
c.passthroughHandler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// We can't handle this request.
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
query := r.URL.Query()
|
|
|
|
switch path {
|
|
case pathReady:
|
|
switch query.Get(queryForce) {
|
|
case "true":
|
|
switch query.Get("ready") {
|
|
case "true":
|
|
c.readyOverride.enable(StatusPass)
|
|
case "false":
|
|
c.readyOverride.enable(StatusFail)
|
|
}
|
|
case "false":
|
|
c.readyOverride.disable()
|
|
}
|
|
writeResponse(w, c.CheckReady(ctx))
|
|
case pathHealth:
|
|
switch query.Get(queryForce) {
|
|
case "true":
|
|
switch query.Get("healthy") {
|
|
case "true":
|
|
c.healthOverride.enable(StatusPass)
|
|
case "false":
|
|
c.healthOverride.enable(StatusFail)
|
|
}
|
|
case "false":
|
|
c.healthOverride.disable()
|
|
}
|
|
writeResponse(w, c.CheckHealth(ctx))
|
|
}
|
|
}
|
|
|
|
// writeResponse writes a Response to the wire as JSON. The HTTP status code
|
|
// accompanying the payload is the primary means for signaling the status of the
|
|
// checks. The possible status codes are:
|
|
//
|
|
// - 200 OK: All checks pass.
|
|
// - 503 Service Unavailable: Some checks are failing.
|
|
// - 500 Internal Server Error: There was a problem serializing the Response.
|
|
func writeResponse(w http.ResponseWriter, resp Response) {
|
|
status := http.StatusOK
|
|
if resp.Status == StatusFail {
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
|
|
msg, err := json.MarshalIndent(resp, "", " ")
|
|
if err != nil {
|
|
msg = []byte(`{"message": "error marshaling response", "status": "fail"}`)
|
|
status = http.StatusInternalServerError
|
|
}
|
|
w.WriteHeader(status)
|
|
fmt.Fprintln(w, string(msg))
|
|
}
|
|
|
|
// override is a manual override for an entire group of checks.
|
|
type override struct {
|
|
mtx sync.Mutex
|
|
status Status
|
|
active bool
|
|
}
|
|
|
|
// get returns the Status of an override as well as whether or not an override
|
|
// is currently active.
|
|
func (m *override) get() (Status, bool) {
|
|
m.mtx.Lock()
|
|
defer m.mtx.Unlock()
|
|
return m.status, m.active
|
|
}
|
|
|
|
// disable disables the override.
|
|
func (m *override) disable() {
|
|
m.mtx.Lock()
|
|
m.active = false
|
|
m.status = StatusFail
|
|
m.mtx.Unlock()
|
|
}
|
|
|
|
// enable turns on the override and establishes a specific Status for which to.
|
|
func (m *override) enable(s Status) {
|
|
m.mtx.Lock()
|
|
m.active = true
|
|
m.status = s
|
|
m.mtx.Unlock()
|
|
}
|