-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhealth.go
More file actions
137 lines (119 loc) · 3.86 KB
/
health.go
File metadata and controls
137 lines (119 loc) · 3.86 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
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
// Package health provides an HTTP health check server.
package health
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net"
"net/http"
"time"
)
// Checker reports whether a component is healthy.
type Checker interface {
Healthy() error
}
// Response is the JSON body returned by the health endpoint.
type Response struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
// Server is a lightweight HTTP server that exposes a /health endpoint.
type Server struct {
httpServer *http.Server
logger *slog.Logger
}
// NewServer creates a new health check server.
// The checker is called on each request to determine upstream health.
func NewServer(addr string, checker Checker, logger *slog.Logger) *Server {
mux := http.NewServeMux()
s := &Server{
httpServer: &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
},
logger: logger,
}
mux.HandleFunc("/health", s.handleHealth(checker))
return s
}
func (s *Server) handleHealth(checker Checker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := checker.Healthy(); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
resp := Response{Status: "unhealthy", Error: err.Error()}
if encErr := json.NewEncoder(w).Encode(resp); encErr != nil {
s.logger.Error("failed to encode health response", "error", encErr)
}
return
}
w.WriteHeader(http.StatusOK)
resp := Response{Status: "ok"}
if err := json.NewEncoder(w).Encode(resp); err != nil {
s.logger.Error("failed to encode health response", "error", err)
}
}
}
// ListenAndServe starts the health server. It blocks until the server
// is shut down or encounters a fatal error. Use Listen + Serve to
// separate binding from serving, which allows detecting bind errors early.
func (s *Server) ListenAndServe() error {
s.logger.Info("health server listening", "addr", s.httpServer.Addr)
err := s.httpServer.ListenAndServe()
if err == http.ErrServerClosed {
return nil
}
return err
}
// Listen binds the server to its configured address. Call Serve to
// start accepting connections after Listen returns successfully.
func (s *Server) Listen() (net.Listener, error) {
ln, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return nil, err
}
s.logger.Info("health server listening", "addr", ln.Addr())
return ln, nil
}
// Serve accepts connections on the given listener. It blocks until the
// server is shut down or encounters a fatal error.
func (s *Server) Serve(ln net.Listener) error {
err := s.httpServer.Serve(ln)
if err == http.ErrServerClosed {
return nil
}
return err
}
// Shutdown gracefully shuts down the health server.
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}
// CheckHealth performs an HTTP health check against the given address.
// It returns nil if the endpoint responds with 200 OK.
// Wildcard listen addresses (e.g. ":8080", "0.0.0.0:8080", "[::]:8080") are
// normalized to localhost so they can be used as dial targets. IPv6 addresses
// are handled correctly via net.JoinHostPort.
func CheckHealth(addr string) error {
// Resolve the address so we can build a proper URL.
host, port, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("invalid address %q: %w", addr, err)
}
// Normalize wildcard and empty hosts to localhost.
if host == "" || host == "0.0.0.0" || host == "::" {
host = "localhost"
}
url := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, port))
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("health check request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check returned status %d", resp.StatusCode)
}
return nil
}