-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
healthz.go
207 lines (174 loc) · 6.29 KB
/
healthz.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
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package healthz
import (
"fmt"
"net/http"
"path"
"sort"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
)
// Handler is an http.Handler that aggregates the results of the given
// checkers to the root path, and supports calling individual checkers on
// subpaths of the name of the checker.
//
// Adding checks on the fly is *not* threadsafe -- use a wrapper.
type Handler struct {
Checks map[string]Checker
}
// checkStatus holds the output of a particular check
type checkStatus struct {
name string
healthy bool
excluded bool
}
func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
failed := false
excluded := getExcludedChecks(req)
parts := make([]checkStatus, 0, len(h.Checks))
// calculate the results...
for checkName, check := range h.Checks {
// no-op the check if we've specified we want to exclude the check
if excluded.Has(checkName) {
excluded.Delete(checkName)
parts = append(parts, checkStatus{name: checkName, healthy: true, excluded: true})
continue
}
if err := check(req); err != nil {
log.V(1).Info("healthz check failed", "checker", checkName, "error", err)
parts = append(parts, checkStatus{name: checkName, healthy: false})
failed = true
} else {
parts = append(parts, checkStatus{name: checkName, healthy: true})
}
}
// ...default a check if none is present...
if len(h.Checks) == 0 {
parts = append(parts, checkStatus{name: "ping", healthy: true})
}
for _, c := range excluded.List() {
log.V(1).Info("cannot exclude health check, no matches for it", "checker", c)
}
// ...sort to be consistent...
sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
// ...and write out the result
// TODO(directxman12): this should also accept a request for JSON content (via a accept header)
_, forceVerbose := req.URL.Query()["verbose"]
writeStatusesAsText(resp, parts, excluded, failed, forceVerbose)
}
// writeStatusAsText writes out the given check statuses in some semi-arbitrary
// bespoke text format that we copied from Kubernetes. unknownExcludes lists
// any checks that the user requested to have excluded, but weren't actually
// known checks. writeStatusAsText is always verbose on failure, and can be
// forced to be verbose on success using the given argument.
func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.String, failed, forceVerbose bool) {
resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
resp.Header().Set("X-Content-Type-Options", "nosniff")
// always write status code first
if failed {
resp.WriteHeader(http.StatusInternalServerError)
} else {
resp.WriteHeader(http.StatusOK)
}
// shortcut for easy non-verbose success
if !failed && !forceVerbose {
fmt.Fprint(resp, "ok")
return
}
// we're always verbose on failure, so from this point on we're guaranteed to be verbose
for _, checkOut := range parts {
switch {
case checkOut.excluded:
fmt.Fprintf(resp, "[+]%s excluded: ok\n", checkOut.name)
case checkOut.healthy:
fmt.Fprintf(resp, "[+]%s ok\n", checkOut.name)
default:
// don't include the error since this endpoint is public. If someone wants more detail
// they should have explicit permission to the detailed checks.
fmt.Fprintf(resp, "[-]%s failed: reason withheld\n", checkOut.name)
}
}
if unknownExcludes.Len() > 0 {
fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.List()...))
}
if failed {
log.Info("healthz check failed", "statuses", parts)
fmt.Fprintf(resp, "healthz check failed\n")
} else {
fmt.Fprint(resp, "healthz check passed\n")
}
}
func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
// clean up the request (duplicating the internal logic of http.ServeMux a bit)
// clean up the path a bit
reqPath := req.URL.Path
if reqPath == "" || reqPath[0] != '/' {
reqPath = "/" + reqPath
}
// path.Clean removes the trailing slash except for root for us
// (which is fine, since we're only serving one layer of sub-paths)
reqPath = path.Clean(reqPath)
// either serve the root endpoint...
if reqPath == "/" {
h.serveAggregated(resp, req)
return
}
// ...the default check (if nothing else is present)...
if len(h.Checks) == 0 && reqPath[1:] == "ping" {
CheckHandler{Checker: Ping}.ServeHTTP(resp, req)
return
}
// ...or an individual checker
checkName := reqPath[1:] // ignore the leading slash
checker, known := h.Checks[checkName]
if !known {
http.NotFoundHandler().ServeHTTP(resp, req)
return
}
CheckHandler{Checker: checker}.ServeHTTP(resp, req)
}
// CheckHandler is an http.Handler that serves a health check endpoint at the root path,
// based on its checker.
type CheckHandler struct {
Checker
}
func (h CheckHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
err := h.Checker(req)
if err != nil {
http.Error(resp, fmt.Sprintf("internal server error: %v", err), http.StatusInternalServerError)
} else {
fmt.Fprint(resp, "ok")
}
}
// Checker knows how to perform a health check.
type Checker func(req *http.Request) error
// Ping returns true automatically when checked
var Ping Checker = func(_ *http.Request) error { return nil }
// getExcludedChecks extracts the health check names to be excluded from the query param
func getExcludedChecks(r *http.Request) sets.String {
checks, found := r.URL.Query()["exclude"]
if found {
return sets.NewString(checks...)
}
return sets.NewString()
}
// formatQuoted returns a formatted string of the health check names,
// preserving the order passed in.
func formatQuoted(names ...string) string {
quoted := make([]string, 0, len(names))
for _, name := range names {
quoted = append(quoted, fmt.Sprintf("%q", name))
}
return strings.Join(quoted, ",")
}