-
Notifications
You must be signed in to change notification settings - Fork 2
/
healthcheck.go
208 lines (166 loc) · 4.95 KB
/
healthcheck.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package healthz
import (
"context"
"encoding/json"
"net/http"
"time"
)
var (
// Prefix represents the prefix for the health check endpoint.
Prefix = ""
// Endpoint represents the endpoint we'll run the health check endpoint on
Endpoint = "/_healthz"
// Timeout represents the duration after which the health check will timeout
// and respond with a 503 Service Unavailable.
Timeout = 5 * time.Second
// ErrTimeout is used to attach to a test when the test took longer than the
// time specified in Timeout.
ErrTimeout = Error("test took too long")
)
var healthCheckTests = map[string]TestFunc{}
// MiddlewareFunc represents a function that acts as middleware.
type MiddlewareFunc func(http.Handler) http.Handler
// TestFunc represents a function which will be executed when we run the health
// check endpoint.
type TestFunc func(context.Context) (Status, error)
// Error represents a health check error
type Error string
// Error returns the error message of our error type.
func (e Error) Error() string {
return string(e)
}
// Status represents the state of a TestFunc
type Status string
var (
// Available represents the success result state
Available Status = "available"
// Degraded represents a degraded result state
Degraded Status = "degraded"
// Unavailable represents the failure result state
Unavailable Status = "unavailable"
)
// HealthCheck represents the overal health check status of the health check
// request.
type HealthCheck struct {
CheckedAt time.Time `json:"checked_at"`
DurationMs time.Duration `json:"duration_ms"`
Status Status `json:"status"`
Tests map[string]Test `json:"tests"`
}
// Test represents a single health check test. All the tests combined
// form the actual HealthCheck.
type Test struct {
Name string `json:"name"`
DurationMs time.Duration `json:"duration_ms"`
Status Status `json:"status"`
Error Error `json:"error,omitempty"`
}
// NewHandler wraps the given http handler with a /_healthz endpoint.
func NewHandler(dh http.Handler) http.Handler {
return NewHandlerWithMiddleware(dh)
}
// NewHandlerWithMiddleware wraps the given handler with a new health endpoint.
// This health endpoint will be wrapped in the provided middleware.
func NewHandlerWithMiddleware(dh http.Handler, mw ...MiddlewareFunc) http.Handler {
var handler http.Handler
h := http.NewServeMux()
handler = http.HandlerFunc(healthHandler)
for _, mwh := range mw {
handler = mwh(handler)
}
h.Handle(Prefix+Endpoint, handler)
h.Handle("/", dh)
return h
}
// RegisterTest adds a test to the HealthCheck handler. If a tests with the
// given name is already registered, this will panic.
func RegisterTest(name string, test TestFunc) {
if _, ok := healthCheckTests[name]; ok {
panic("Test already registered")
}
healthCheckTests[name] = test
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
start := time.Now()
hc := HealthCheck{
CheckedAt: time.Now(),
Tests: map[string]Test{},
Status: Available,
}
ctx, cancel := context.WithDeadline(r.Context(), time.Now().Add(Timeout))
defer cancel()
rspChan := make(chan Test, len(healthCheckTests))
statuses := []Status{}
for name, test := range healthCheckTests {
go runTest(ctx, name, test, rspChan)
}
for i := 0; i < len(healthCheckTests); i++ {
select {
case rsp := <-rspChan:
statuses = append(statuses, rsp.Status)
hc.Tests[rsp.Name] = rsp
case <-ctx.Done():
w.WriteHeader(http.StatusServiceUnavailable)
hc.Status = Unavailable
for name := range healthCheckTests {
if _, ok := hc.Tests[name]; !ok {
hc.Tests[name] = Test{
Name: name,
Status: Unavailable,
Error: ErrTimeout,
DurationMs: Timeout / time.Millisecond,
}
}
}
handleResponse(w, hc, start)
return
}
}
hc.Status = getOverallStatus(statuses)
switch hc.Status {
case Unavailable:
w.WriteHeader(http.StatusServiceUnavailable)
default:
w.WriteHeader(http.StatusOK)
}
handleResponse(w, hc, start)
}
func handleResponse(w http.ResponseWriter, hc HealthCheck, start time.Time) {
hc.DurationMs = time.Since(start) / time.Millisecond
if err := json.NewEncoder(w).Encode(hc); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
func runTest(ctx context.Context, name string, test TestFunc, rspChan chan Test) {
hct := Test{
Name: name,
Status: Available,
}
tStart := time.Now()
testStatus, err := test(ctx)
if err != nil {
hct.Error = Error(err.Error())
}
hct.Status = testStatus
hct.DurationMs = time.Since(tStart) / time.Millisecond
rspChan <- hct
}
func getOverallStatus(statuses []Status) Status {
status := Available
for _, s := range statuses {
if s == Unavailable {
return s
}
if s == Degraded {
status = Degraded
}
}
return status
}
func defaultCheck(ctx context.Context) (Status, error) {
return Available, nil
}
func init() {
RegisterTest("default", defaultCheck)
}