-
Notifications
You must be signed in to change notification settings - Fork 0
/
healthcheck.go
267 lines (220 loc) · 7.54 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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// Package healthcheck provides types and functions to implement liveness
// and readyness checks based on HTTP probes. The package provides a
// http.Handler that can be mounted using to a running server. Client code can
// register checks to be executed when the readyness endpoint is invoked.
// The package also provides ready to use checks for HTTP endpoints and SQL
// databases.
//
// The handler also reports version information of the running application. This
// is an opt-in feature disabled by default. The version info will be gathered
// using the runtime/debug and can be enhanced with custom fields.
package healthcheck
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime/debug"
"strconv"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
// Check defines the interface for custom readyness checks.
type Check interface {
// Check is called to execute the check. Any non-nil return value
// is considered a check failure incl. context deadlines.
Check(context.Context) error
}
// CheckFunc is a convenience type to implement Check using a bare function.
type CheckFunc func(context.Context) error
func (f CheckFunc) Check(ctx context.Context) error { return f(ctx) }
// --
var ErrURLCheckFailed = errors.New("URL check failed")
// CheckURL creates a Check that checks url for a status code < 400. The returned
// check uses http.DefaultClient to issue the HTTP request.
// Use CheckHTTPResponse when custom handling is needed.
func CheckURL(url string) Check {
return CheckHTTPResponse(http.MethodGet, url, nil)
}
// CheckHTTPResponse creates a Check that issues a HTTP request with method to
// url using client. The check reports an error if either the request fails or
// the received status code is >= 400 (bad request).
// If client is nil http.DefaultClient is used.
func CheckHTTPResponse(method, url string, client *http.Client) Check {
if client == nil {
client = http.DefaultClient
}
return CheckFunc(func(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
if err != nil {
return fmt.Errorf("%w: failed to create http request for %s %s: %s",
ErrURLCheckFailed, method, url, err)
}
res, err := client.Do(req)
if err != nil {
return fmt.Errorf("%w: failed to issue http request for %s %s: %s",
ErrURLCheckFailed, method, url, err)
}
if res.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("%w: got failing status code for %s %s: %d",
ErrURLCheckFailed, method, url, res.StatusCode)
}
return nil
})
}
var ErrPingCheckFailed = errors.New("ping check failed")
// Pinger defines the interface for connection types that support pinging the
// remote endpoint to learn about its liveness. The method name is chosen to
// make a value of type *sql.DB satisfy this interface without any adaption.
type Pinger interface {
// PingContext pings the remote endpoint. It returns nil if the endpoint is
// healthy, a non-nil error otherwise.
PingContext(context.Context) error
}
// CheckPing creates a Check that calls PingContext to check for connectivity.
// This method can directly be used on a *sql.DB.
func CheckPing(pinger Pinger) Check {
return CheckFunc(func(ctx context.Context) error {
if err := pinger.PingContext(ctx); err != nil {
return fmt.Errorf("%w: %s", ErrPingCheckFailed, err)
}
return nil
})
}
// --
// ErrorLogger defines a type for a function to log errors that occured during
// ready check execution. err is the error returned by the check function.
type ErrorLogger func(err error)
// --
var (
// Configures the final path element of the URL serving the liveness check.
// Changes to this variable will only take effect when done before calling New.
LivePath = "/livez"
// Configures the final path element of the URL serving the readyness check.
// Changes to this variable will only take effect when done before calling New.
ReadyPath = "/readyz"
// Configures the final path element of the URL serving the info endpoint.
// Changes to this variable will only take effect when done before calling New.
InfoPath = "/infoz"
// Default timeout to apply to readyness checks
DefaultReadynessCheckTimeout = 10 * time.Second
)
// Option defines a function type used to customize the provided Handler.
type Option func(*Handler)
// WithErrorLogger creates an Option that sets Handler.ErrorLogger to l.
func WithErrorLogger(l ErrorLogger) Option {
return func(h *Handler) {
h.ErrorLogger = l
}
}
// WithReadynessTimeout creates an Option that sets Handler.ReadynessTimeout to
// t.
func WithReadynessTimeout(t time.Duration) Option {
return func(h *Handler) {
h.ReadynessTimeout = t
}
}
// Handler implements liveness and readyness checking.
type Handler struct {
ErrorLogger ErrorLogger
ReadynessTimeout time.Duration
checks []Check
lock sync.RWMutex
mux http.ServeMux
infoPayload []byte
}
// New creates a new Handler ready to use. The Handler must be
// mounted on some HTTP path (i.e. on a http.ServeMux) to receive
// requests.
func New(opts ...Option) *Handler {
h := &Handler{
mux: *http.NewServeMux(),
ReadynessTimeout: DefaultReadynessCheckTimeout,
}
for _, opt := range opts {
if opt != nil {
opt(h)
}
}
h.mux.HandleFunc(LivePath, h.handleLive)
h.mux.HandleFunc(ReadyPath, h.handleReady)
return h
}
// AddCheckFunc registers c as another readyness check.
func (h *Handler) AddCheckFunc(c CheckFunc) {
h.AddCheck(CheckFunc(c))
}
// AddCheck registers c as another readyness check.
func (h *Handler) AddCheck(c Check) {
h.lock.Lock()
defer h.lock.Unlock()
h.checks = append(h.checks, c)
}
// ExecuteReadyChecks executes all readyness checks in parallel. It reports the
// first error hit or nil if all checks pass. Every check is executed with a
// timeout configured for the handler (if any).
func (h *Handler) ExecuteReadyChecks(ctx context.Context) error {
h.lock.RLock()
defer h.lock.RUnlock()
if h.ReadynessTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, h.ReadynessTimeout)
defer cancel()
}
eg, ctx := errgroup.WithContext(ctx)
for _, c := range h.checks {
c := c
eg.Go(func() error { return c.Check(ctx) })
}
if err := eg.Wait(); err != nil {
if h.ErrorLogger != nil {
h.ErrorLogger(err)
}
return err
}
return nil
}
// EnableInfo enables an info endpoint that outputs version information and
// additional details.
func (h *Handler) EnableInfo(infoData map[string]any) {
if infoData == nil {
infoData = make(map[string]any)
}
info, ok := debug.ReadBuildInfo()
if ok {
infoData["version"] = info.Main.Version
settings := make(map[string]any)
for _, s := range info.Settings {
settings[s.Key] = s.Value
}
infoData["build_settings"] = settings
}
var err error
h.infoPayload, err = json.Marshal(infoData)
if err != nil {
panic(err)
}
h.mux.HandleFunc(InfoPath, h.handleInfo)
}
// ServeHTTP dispatches and executes health checks.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
func (h *Handler) handleLive(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) handleReady(w http.ResponseWriter, r *http.Request) {
if err := h.ExecuteReadyChecks(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) handleInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Header().Set("Content-Length", strconv.Itoa(len(h.infoPayload)))
w.WriteHeader(http.StatusOK)
w.Write(h.infoPayload)
}