forked from hellofresh/health-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
health.go
278 lines (241 loc) · 6.61 KB
/
health.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
268
269
270
271
272
273
274
275
276
277
278
package health
import (
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"os"
"runtime"
"sync"
"time"
)
type Health struct {
mu sync.Mutex
checkMap map[string]Config
//WithSysMetrics is the conditional to process system metrics
//If it true log and response system metrics
WithSysMetrics bool
// ErrorLogFunc is the callback function for errors logging during check.
// If not set logging is skipped.
ErrorLogFunc func(err error, details string, extra ...interface{})
// DebugLogFunc is the callback function for debug logging during check.
// If not set logging is skipped.
DebugLogFunc func(...interface{})
}
func New(WithSysMetrics bool, ErrorLogFunc func(err error, details string, extra ...interface{}), DebugLogFunc func(...interface{})) Health {
if ErrorLogFunc == nil {
ErrorLogFunc = func(err error, details string, extra ...interface{}) {}
}
if DebugLogFunc == nil {
DebugLogFunc = func(...interface{}) {}
}
return Health{
mu: sync.Mutex{},
checkMap: make(map[string]Config),
WithSysMetrics: WithSysMetrics,
ErrorLogFunc: ErrorLogFunc,
DebugLogFunc: DebugLogFunc,
}
}
const (
statusOK = "OK"
statusPartiallyAvailable = "Partially Available"
statusUnavailable = "Unavailable"
failureTimeout = "Timeout during health check"
)
type (
// CheckFunc is the func which executes the check.
CheckFunc func() error
// Config carries the parameters to run the check.
Config struct {
// Name is the name of the resource to be checked.
Name string
// Timeout is the timeout defined for every check.
Timeout time.Duration
// SkipOnErr if set to true, it will retrieve StatusOK providing the error message from the failed resource.
SkipOnErr bool
// Check is the func which executes the check.
Check CheckFunc
}
// Check represents the health check response.
Check struct {
// Status is the check status.
Status string `json:"status"`
// Timestamp is the time in which the check occurred.
Timestamp time.Time `json:"timestamp"`
// Failures holds the failed checks along with their messages.
Failures map[string]string `json:"failures,omitempty"`
// System holds information of the go process.
System `json:"system"`
}
// System runtime variables about the go process.
System struct {
// Version is the go version.
Version string `json:"version"`
// GoroutinesCount is the number of the current goroutines.
GoroutinesCount int `json:"goroutines_count"`
// TotalAllocBytes is the total bytes allocated.
TotalAllocBytes int `json:"total_alloc_bytes"`
// HeapObjectsCount is the number of objects in the go heap.
HeapObjectsCount int `json:"heap_objects_count"`
// TotalAllocBytes is the bytes allocated and not yet freed.
AllocBytes int `json:"alloc_bytes"`
}
checkResponse struct {
name string
skipOnErr bool
err error
}
)
// Register allot of checks
func (h *Health) BulkRegister(c ...Config) (err error) {
for _, cf := range c {
err = h.Register(cf)
if err != nil {
return
}
}
return
}
// Register registers a check config to be performed.
func (h *Health) Register(c Config) error {
if c.Timeout == 0 {
c.Timeout = time.Second * 2
}
if c.Name == "" {
return errors.New("health check must have a name to be registered")
}
h.mu.Lock()
defer h.mu.Unlock()
if _, ok := h.checkMap[c.Name]; ok {
return fmt.Errorf("health check %s is already registered", c.Name)
}
h.checkMap[c.Name] = c
return nil
}
//Execute a health check standalone if the flag chosen is true
func (h *Health) HealthCheckStandaloneMode(flagName string) {
b := flag.Bool(flagName, false, "Flag used to ability the health check mode")
flag.Parse()
if *b {
h.ExecuteStandalone()
}
}
// Handler returns an HTTP handler (http.HandlerFunc).
func (h *Health) Handler() http.Handler {
return http.HandlerFunc(h.HandlerFunc)
}
// HandlerFunc is the HTTP handler function.
func (h *Health) HandlerFunc(w http.ResponseWriter, r *http.Request) {
h.DebugLogFunc("Handling health check func")
c := h.ExecuteCheck()
h.logCheck(c)
data, err := json.Marshal(c)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
h.ErrorLogFunc(err, "Error to parse health check result")
return
}
code := http.StatusOK
if c.Status == statusUnavailable {
code = http.StatusServiceUnavailable
}
w.WriteHeader(code)
_, _ = w.Write(data)
}
//Execute health check base on config map
func (h *Health) ExecuteCheck() Check {
h.mu.Lock()
defer h.mu.Unlock()
status := statusOK
total := len(h.checkMap)
failures := make(map[string]string)
resChan := make(chan checkResponse, total)
var wg sync.WaitGroup
wg.Add(total)
go func() {
defer close(resChan)
wg.Wait()
}()
for _, c := range h.checkMap {
go func(c Config) {
h.DebugLogFunc("Executing health check:", c.Name)
defer wg.Done()
select {
case resChan <- checkResponse{c.Name, c.SkipOnErr, c.Check()}:
default:
}
}(c)
loop:
for {
select {
case <-time.After(c.Timeout):
failures[c.Name] = failureTimeout
setStatus(&status, c.SkipOnErr)
break loop
case res := <-resChan:
if res.err != nil {
failures[res.name] = res.err.Error()
setStatus(&status, res.skipOnErr)
}
break loop
}
}
}
c := h.newCheck(status, failures)
return c
}
//Execute health check in standalone which if it is not ok return code 1 to system
func (h *Health) ExecuteStandalone() {
c := h.ExecuteCheck()
h.logCheck(c)
if c.Status == statusOK {
os.Exit(0)
}
os.Exit(1)
}
// Reset unregisters all previously set check configs
func (h *Health) Reset() {
h.mu.Lock()
h.DebugLogFunc("Reseting health check configs")
defer h.mu.Unlock()
h.checkMap = make(map[string]Config)
}
func (h *Health) logCheck(c Check) {
b, e := json.MarshalIndent(c, "", " ")
if e != nil {
h.ErrorLogFunc(e, "Error to parse Health Check Result")
}
h.DebugLogFunc("Health Check Result:\n", string(b))
}
func (h *Health) newCheck(status string, failures map[string]string) Check {
c := Check{
Status: status,
Timestamp: time.Now(),
Failures: failures,
}
if h.WithSysMetrics {
c.System = newSystemMetrics()
}
return c
}
func newSystemMetrics() System {
s := runtime.MemStats{}
runtime.ReadMemStats(&s)
return System{
Version: runtime.Version(),
GoroutinesCount: runtime.NumGoroutine(),
TotalAllocBytes: int(s.TotalAlloc),
HeapObjectsCount: int(s.HeapObjects),
AllocBytes: int(s.Alloc),
}
}
func setStatus(status *string, skipOnErr bool) {
if skipOnErr && *status != statusUnavailable {
*status = statusPartiallyAvailable
} else {
*status = statusUnavailable
}
}