-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathhealth_check.go
More file actions
111 lines (92 loc) · 3.49 KB
/
health_check.go
File metadata and controls
111 lines (92 loc) · 3.49 KB
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
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/
package httpserver
import (
"context"
"errors"
"net/http"
"github.com/acronis/go-appkit/httpserver/middleware"
"github.com/acronis/go-appkit/log"
"github.com/acronis/go-appkit/restapi"
)
// StatusClientClosedRequest is a special HTTP status code used by Nginx to show that the client
// closed the request before the server could send a response
const StatusClientClosedRequest = 499
// HealthCheckComponentName is a type alias for component names. It's used for better readability.
type HealthCheckComponentName = string
// HealthCheckStatus is a resulting status of the health-check.
type HealthCheckStatus int
// Health-check statuses.
const (
HealthCheckStatusOK HealthCheckStatus = iota
HealthCheckStatusFail
)
// HealthCheckResult is a type alias for result of health-check operation. It's used for better readability.
type HealthCheckResult = map[HealthCheckComponentName]HealthCheckStatus
// HealthCheck is a type alias for health-check operation. It's used for better readability.
type HealthCheck = func() (HealthCheckResult, error)
// HealthCheckContext is a type alias for health-check operation that has access to the request Context
type HealthCheckContext = func(ctx context.Context) (HealthCheckResult, error)
type healthCheckResponseData struct {
Components map[string]bool `json:"components"`
}
// HealthCheckHandler implements http.Handler and does health-check of a service.
type HealthCheckHandler struct {
healthCheckFn HealthCheckContext
}
// NewHealthCheckHandler creates a new http.Handler for doing health-check.
// Passing function will be called inside handler and should return statuses of service's components.
func NewHealthCheckHandler(fn HealthCheck) *HealthCheckHandler {
if fn == nil {
fn = func() (HealthCheckResult, error) {
return HealthCheckResult{}, nil
}
}
return &HealthCheckHandler{func(_ context.Context) (HealthCheckResult, error) {
return fn()
}}
}
// NewHealthCheckHandlerContext creates a new http.Handler for doing health-check. It is able to access the request Context.
// Passing function will be called inside handler and should return statuses of service's components.
func NewHealthCheckHandlerContext(fn HealthCheckContext) *HealthCheckHandler {
if fn == nil {
fn = func(ctx context.Context) (HealthCheckResult, error) {
return HealthCheckResult{}, ctx.Err()
}
}
return &HealthCheckHandler{fn}
}
// ServeHTTP serves heath-check HTTP request.
func (h *HealthCheckHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
hcResult, err := h.healthCheckFn(r.Context())
if err != nil {
if logger := middleware.GetLoggerFromContext(r.Context()); logger != nil {
logger.Error("error while checking health", log.Error(err))
}
if errors.Is(err, context.Canceled) {
rw.WriteHeader(StatusClientClosedRequest)
return
}
rw.WriteHeader(http.StatusInternalServerError)
return
}
hasUnhealthyComponent := false
respData := healthCheckResponseData{Components: map[string]bool{}}
for name, status := range hcResult {
respData.Components[name] = status == HealthCheckStatusOK
if status == HealthCheckStatusFail {
hasUnhealthyComponent = true
}
}
if errors.Is(r.Context().Err(), context.Canceled) {
rw.WriteHeader(StatusClientClosedRequest)
return
}
respStatus := http.StatusOK
if hasUnhealthyComponent {
respStatus = http.StatusServiceUnavailable
}
restapi.RespondCodeAndJSON(rw, respStatus, respData, middleware.GetLoggerFromContext(r.Context()))
}