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}