health.go
1package rtw
2
3import (
4 "encoding/json"
5 "io"
6 "log/slog"
7 "net/http"
8 "runtime/debug"
9 "time"
10)
11
12// getBuildInfo returns version control metadata embedded at build time.
13func getBuildInfo() map[string]string {
14 info, ok := debug.ReadBuildInfo()
15 if !ok {
16 return map[string]string{"error": "could not read build info"}
17 }
18
19 buildInfoMap := make(map[string]string)
20
21 for _, setting := range info.Settings {
22 // Include vcs information like commit, dirty status, etc.
23 if setting.Key == "vcs.revision" ||
24 setting.Key == "vcs.time" ||
25 setting.Key == "vcs.modified" {
26 buildInfoMap[setting.Key] = setting.Value
27 }
28 }
29
30 return buildInfoMap
31}
32
33// checkWaldoHealth queries the Waldo geocoder /health endpoint.
34func checkWaldoHealth(client *http.Client, url string) map[string]interface{} {
35 waldoHealth := make(map[string]interface{})
36 if url == "" {
37 waldoHealth["status"] = "not_configured"
38 return waldoHealth
39 }
40 resp, err := client.Get(url + "/health")
41 if err != nil {
42 waldoHealth["status"] = "unreachable"
43 waldoHealth["error"] = err.Error()
44 return waldoHealth
45 }
46 defer resp.Body.Close()
47 if resp.StatusCode != http.StatusOK {
48 waldoHealth["status"] = "unhealthy"
49 waldoHealth["http_status"] = resp.StatusCode
50 body, _ := io.ReadAll(resp.Body)
51 waldoHealth["error"] = string(body)
52 return waldoHealth
53 }
54 if err := json.NewDecoder(resp.Body).Decode(&waldoHealth); err != nil {
55 waldoHealth["status"] = "error"
56 waldoHealth["error"] = err.Error()
57 return waldoHealth
58 }
59 return waldoHealth
60}
61
62// checkRoutingHealth queries the GraphHopper-compatible routing /health endpoint.
63// The endpoint returns 200 with a text/plain body "OK".
64func checkRoutingHealth(client *http.Client, url string) string {
65 if url == "" {
66 return "not_configured"
67 }
68 resp, err := client.Get(url + "/health")
69 if err != nil {
70 return "unreachable"
71 }
72 defer resp.Body.Close()
73 if resp.StatusCode != http.StatusOK {
74 return "unhealthy"
75 }
76 body, err := io.ReadAll(resp.Body)
77 if err != nil {
78 return "error"
79 }
80 if string(body) == "OK" {
81 return "OK"
82 }
83 return "unexpected_response"
84}
85
86// checkDocumentsHealth queries the YSweet /ready endpoint.
87// The endpoint returns JSON {"ok": boolean}.
88func checkDocumentsHealth(client *http.Client, url string) string {
89 if url == "" {
90 return "not_configured"
91 }
92 resp, err := client.Get(url + "/ready")
93 if err != nil {
94 return "unreachable"
95 }
96 defer resp.Body.Close()
97 if resp.StatusCode != http.StatusOK {
98 return "unhealthy"
99 }
100 var result struct {
101 Ok bool `json:"ok"`
102 }
103 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
104 return "error"
105 }
106 if result.Ok {
107 return "OK"
108 }
109 return "not_ready"
110}
111
112// AllServicesHealthy returns true when waldo, routing, and documents all report healthy.
113func AllServicesHealthy(cfg *Config) bool {
114 client := &http.Client{Timeout: 5 * time.Second}
115 waldo := checkWaldoHealth(client, cfg.Geocoder.URL)
116 if waldo["status"] != "healthy" {
117 return false
118 }
119 if checkRoutingHealth(client, cfg.Routing.URL) != "OK" {
120 return false
121 }
122 if checkDocumentsHealth(client, cfg.YSweet.URL) != "OK" {
123 return false
124 }
125 return true
126}
127
128// healthHandler returns an HTTP handler that reports server health, build metadata,
129// and the health of the upstream Waldo, routing, and document services.
130func healthHandler(start time.Time, build map[string]string, cfg *Config) http.HandlerFunc {
131 client := &http.Client{Timeout: 5 * time.Second}
132 return func(w http.ResponseWriter, r *http.Request) {
133 response := make(map[string]interface{})
134 for k, v := range build {
135 response[k] = v
136 }
137 response["uptime"] = time.Since(start).String()
138 response["status"] = "ok"
139
140 response["waldo"] = checkWaldoHealth(client, cfg.Geocoder.URL)
141 response["routing"] = checkRoutingHealth(client, cfg.Routing.URL)
142 response["documents"] = checkDocumentsHealth(client, cfg.YSweet.URL)
143
144 w.Header().Set("Content-Type", "application/json")
145 w.WriteHeader(http.StatusOK)
146
147 if err := json.NewEncoder(w).Encode(response); err != nil {
148 slog.Error("error encoding health response", "error", err)
149 }
150 }
151}