133 lines
3.3 KiB
Go
133 lines
3.3 KiB
Go
|
|
package health
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Level represents the criticality of a component to the overall application health.
|
||
|
|
type Level int
|
||
|
|
|
||
|
|
const (
|
||
|
|
// LevelCritical indicates a component essential for the application.
|
||
|
|
// If any critical component is DOWN, the overall status is DOWN (503).
|
||
|
|
LevelCritical Level = iota
|
||
|
|
// LevelDegraded indicates a component that is important but not essential.
|
||
|
|
// If a degraded component is DOWN, the overall status is DEGRADED (200).
|
||
|
|
LevelDegraded
|
||
|
|
)
|
||
|
|
|
||
|
|
// Checkable is the interface that infrastructure components implement.
|
||
|
|
type Checkable interface {
|
||
|
|
HealthCheck(ctx context.Context) error
|
||
|
|
Name() string
|
||
|
|
Priority() Level
|
||
|
|
}
|
||
|
|
|
||
|
|
// Logger is the minimal interface health needs — satisfied by logz.Logger via duck typing.
|
||
|
|
type Logger interface {
|
||
|
|
Debug(msg string, args ...any)
|
||
|
|
Info(msg string, args ...any)
|
||
|
|
Warn(msg string, args ...any)
|
||
|
|
Error(msg string, err error, args ...any)
|
||
|
|
WithContext(ctx context.Context) Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
// ComponentStatus represents the health state of an individual component.
|
||
|
|
type ComponentStatus struct {
|
||
|
|
Status string `json:"status"`
|
||
|
|
Latency string `json:"latency,omitempty"`
|
||
|
|
Error string `json:"error,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Response is the JSON body returned by the health handler.
|
||
|
|
type Response struct {
|
||
|
|
Status string `json:"status"`
|
||
|
|
Components map[string]ComponentStatus `json:"components"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type handler struct {
|
||
|
|
logger Logger
|
||
|
|
checks []Checkable
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewHandler returns an http.Handler for the health endpoint.
|
||
|
|
// Runs all checks concurrently with a 5-second timeout.
|
||
|
|
// Returns 200 (UP/DEGRADED) or 503 (DOWN).
|
||
|
|
func NewHandler(logger Logger, checks ...Checkable) http.Handler {
|
||
|
|
return &handler{logger: logger, checks: checks}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
|
|
logger := h.logger.WithContext(r.Context())
|
||
|
|
logger.Debug("health: running checks")
|
||
|
|
|
||
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
type result struct {
|
||
|
|
name string
|
||
|
|
status ComponentStatus
|
||
|
|
priority Level
|
||
|
|
}
|
||
|
|
|
||
|
|
resChan := make(chan result, len(h.checks))
|
||
|
|
|
||
|
|
for _, check := range h.checks {
|
||
|
|
go func(chk Checkable) {
|
||
|
|
start := time.Now()
|
||
|
|
err := chk.HealthCheck(ctx)
|
||
|
|
latency := time.Since(start).String()
|
||
|
|
|
||
|
|
status := "UP"
|
||
|
|
errMsg := ""
|
||
|
|
if err != nil {
|
||
|
|
errMsg = err.Error()
|
||
|
|
if chk.Priority() == LevelDegraded {
|
||
|
|
status = "DEGRADED"
|
||
|
|
} else {
|
||
|
|
status = "DOWN"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
resChan <- result{
|
||
|
|
name: chk.Name(),
|
||
|
|
priority: chk.Priority(),
|
||
|
|
status: ComponentStatus{
|
||
|
|
Status: status,
|
||
|
|
Latency: latency,
|
||
|
|
Error: errMsg,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}(check)
|
||
|
|
}
|
||
|
|
|
||
|
|
overallStatus := "UP"
|
||
|
|
httpStatus := http.StatusOK
|
||
|
|
components := make(map[string]ComponentStatus)
|
||
|
|
|
||
|
|
for range h.checks {
|
||
|
|
res := <-resChan
|
||
|
|
components[res.name] = res.status
|
||
|
|
|
||
|
|
switch res.status.Status {
|
||
|
|
case "DOWN":
|
||
|
|
overallStatus = "DOWN"
|
||
|
|
httpStatus = http.StatusServiceUnavailable
|
||
|
|
case "DEGRADED":
|
||
|
|
if overallStatus == "UP" {
|
||
|
|
overallStatus = "DEGRADED"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
resp := Response{Status: overallStatus, Components: components}
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(httpStatus)
|
||
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||
|
|
logger.Error("health: failed to encode response", err)
|
||
|
|
}
|
||
|
|
}
|