-
Notifications
You must be signed in to change notification settings - Fork 220
Add /health endpoint for comprehensive service readiness checking #701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 13 commits
e9cf9a7
f7916f0
bca84c3
5a4091f
5903542
f4e78bb
e6f5df7
31c846f
0172a76
f43ffa2
c77de6f
1da0bc0
cac8f1f
aa5ddd0
6165455
1f0c42b
eae11d3
2483658
f0e6cb5
b6aa4c1
0d12bb3
d66793e
84042bc
0a7c8e8
553b508
759181a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| __pycache__/ | ||
| *.pyc | ||
| test_health_endpoint | ||
sagpatil marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ EXPOSE 6060 | |
| EXPOSE 6061 | ||
| EXPOSE 8000 | ||
| EXPOSE 8002 | ||
| EXPOSE 8004 | ||
| EXPOSE 8100 | ||
| EXPOSE 11625 | ||
| EXPOSE 11626 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| location /health { | ||
| rewrite /health / break; | ||
| proxy_set_header Host $http_host; | ||
| proxy_pass http://127.0.0.1:8004; | ||
| proxy_redirect off; | ||
| } |
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
| ) | ||
|
|
||
| type HealthResponse struct { | ||
| Status string `json:"status"` | ||
| Services map[string]string `json:"services"` | ||
| } | ||
|
|
||
| type RPCResponse struct { | ||
| Result struct { | ||
| Status string `json:"status"` | ||
| } `json:"result"` | ||
| Error struct { | ||
| Message string `json:"message"` | ||
| } `json:"error"` | ||
| } | ||
|
|
||
| type HorizonRoot struct { | ||
| SupportedProtocolVersion int32 `json:"supported_protocol_version"` | ||
| CoreLatestLedger int32 `json:"core_latest_ledger"` | ||
| HistoryLatestLedger int32 `json:"history_latest_ledger"` | ||
| } | ||
|
|
||
| func main() { | ||
| log.Println("Starting health service on port 8004") | ||
|
|
||
| http.HandleFunc("/", healthHandler) | ||
| if err := http.ListenAndServe(":8004", nil); err != nil { | ||
| log.Fatal("Health service failed to start:", err) | ||
| } | ||
| } | ||
|
|
||
| func healthHandler(w http.ResponseWriter, r *http.Request) { | ||
| response := HealthResponse{ | ||
| Status: "healthy", | ||
| Services: make(map[string]string), | ||
| } | ||
|
|
||
| enableCore := os.Getenv("ENABLE_CORE") == "true" | ||
| enableHorizon := os.Getenv("ENABLE_HORIZON") == "true" | ||
| enableRPC := os.Getenv("ENABLE_RPC") == "true" | ||
|
|
||
| allHealthy := true | ||
|
|
||
| // Check stellar-core if enabled | ||
| if enableCore { | ||
| if coreHealthy := checkStellarCore(); coreHealthy { | ||
| response.Services["stellar-core"] = "healthy" | ||
| } else { | ||
| response.Services["stellar-core"] = "unhealthy" | ||
| allHealthy = false | ||
| } | ||
| } | ||
|
|
||
| // Check horizon if enabled | ||
| if enableHorizon { | ||
| if horizonHealthy := checkHorizon(); horizonHealthy { | ||
| response.Services["horizon"] = "healthy" | ||
| } else { | ||
| response.Services["horizon"] = "unhealthy" | ||
| allHealthy = false | ||
| } | ||
| } | ||
|
|
||
| // Check stellar-rpc if enabled | ||
| if enableRPC { | ||
| if rpcHealthy := checkStellarRPC(); rpcHealthy { | ||
| response.Services["stellar-rpc"] = "healthy" | ||
| } else { | ||
| response.Services["stellar-rpc"] = "unhealthy" | ||
| allHealthy = false | ||
| } | ||
| } | ||
|
|
||
| if !allHealthy { | ||
| response.Status = "unhealthy" | ||
| w.WriteHeader(http.StatusServiceUnavailable) | ||
| } else { | ||
| w.WriteHeader(http.StatusOK) | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(response) | ||
| } | ||
|
|
||
| func checkStellarCore() bool { | ||
| client := &http.Client{Timeout: 5 * time.Second} | ||
| resp, err := client.Get("http://localhost:11626/info") | ||
| if err != nil { | ||
| log.Printf("stellar-core check failed: %v", err) | ||
| return false | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| log.Printf("stellar-core returned status: %d", resp.StatusCode) | ||
| return false | ||
| } | ||
|
|
||
| // For now, just check if the service responds | ||
| // Could be enhanced to check sync status | ||
| return true | ||
| } | ||
|
|
||
| func checkHorizon() bool { | ||
| client := &http.Client{Timeout: 5 * time.Second} | ||
| resp, err := client.Get("http://localhost:8001") | ||
| if err != nil { | ||
| log.Printf("horizon check failed: %v", err) | ||
| return false | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| log.Printf("horizon returned status: %d", resp.StatusCode) | ||
| return false | ||
| } | ||
|
|
||
| var root HorizonRoot | ||
| decoder := json.NewDecoder(resp.Body) | ||
| err = decoder.Decode(&root) | ||
| if err != nil { | ||
| log.Printf("horizon response decode failed: %v", err) | ||
| return false | ||
| } | ||
|
|
||
| // Check that horizon is properly started and ingesting | ||
| return root.SupportedProtocolVersion > 0 && root.CoreLatestLedger > 0 && root.HistoryLatestLedger > 0 | ||
| } | ||
|
|
||
| func checkStellarRPC() bool { | ||
| client := &http.Client{Timeout: 5 * time.Second} | ||
|
|
||
| getHealthRPCRequest := []byte(`{ | ||
| "jsonrpc": "2.0", | ||
| "id": 10235, | ||
| "method": "getHealth" | ||
| }`) | ||
|
|
||
| resp, err := client.Post("http://localhost:8003", "application/json", bytes.NewBuffer(getHealthRPCRequest)) | ||
| if err != nil { | ||
| log.Printf("stellar-rpc check failed: %v", err) | ||
| return false | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| log.Printf("stellar-rpc returned status: %d", resp.StatusCode) | ||
| return false | ||
| } | ||
|
|
||
| var rpcResponse RPCResponse | ||
| decoder := json.NewDecoder(resp.Body) | ||
| err = decoder.Decode(&rpcResponse) | ||
| if err != nil { | ||
| log.Printf("stellar-rpc response decode failed: %v", err) | ||
| return false | ||
| } | ||
|
|
||
| return rpcResponse.Result.Status == "healthy" | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.