Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e9cf9a7
Initial plan
Copilot Jul 10, 2025
f7916f0
Add health endpoint service and configuration
Copilot Jul 10, 2025
bca84c3
Rename health endpoint to /ready to avoid conflicts with Horizon's /h…
Copilot Jul 10, 2025
5a4091f
Complete working implementation of /ready endpoint
Copilot Jul 10, 2025
5903542
Add documentation for new /ready endpoint
Copilot Jul 10, 2025
f4e78bb
Rename /ready endpoint to /health per feedback
Copilot Jul 11, 2025
e6f5df7
Complete renaming of endpoint to /health - task finished
Copilot Jul 11, 2025
31c846f
Clean up build artifacts from git history
Copilot Jul 11, 2025
0172a76
Merge branch 'main' into copilot/fix-683
sagpatil Jul 11, 2025
f43ffa2
Resolve merge conflicts with main branch
Copilot Aug 18, 2025
c77de6f
Fix typo in build-testing.yml and resolve merge conflicts
Copilot Aug 18, 2025
1da0bc0
resolve merge conflict in readme file
sagpatil Aug 19, 2025
cac8f1f
Merge branch 'main' into copilot/fix-683
sagpatil Aug 19, 2025
aa5ddd0
Adding better testing
sagpatil Aug 20, 2025
6165455
Add Test to CI Pipeline
sagpatil Aug 20, 2025
1f0c42b
Attempt to Fix CI health endpoint test and add debugging
sagpatil Aug 20, 2025
eae11d3
Fix health endpoint test to use proper /health endpoint through nginx
Copilot Aug 21, 2025
2483658
Update README.md
sagpatil Aug 21, 2025
f0e6cb5
update git ignore file
sagpatil Aug 21, 2025
b6aa4c1
consistency in calling the health endpoint test in CI
sagpatil Aug 21, 2025
0d12bb3
Merge branch 'main' into copilot/fix-683
sagpatil Aug 21, 2025
d66793e
extend timeout
sagpatil Aug 21, 2025
84042bc
revert timeout and better start
sagpatil Aug 21, 2025
0a7c8e8
fix typo
sagpatil Aug 21, 2025
553b508
make readiness service more lenient during startup and readme changes
sagpatil Aug 21, 2025
759181a
remove unused script adn udpate readme file
sagpatil Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
friendbot_ref: horizon-v23.0.0
lab_ref: main
test_matrix: |
{j
{
"network": ["testnet", "pubnet", "local"],
"core": ["core", null],
"horizon": ["horizon", null],
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
test_health_endpoint
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ EXPOSE 6060
EXPOSE 6061
EXPOSE 8000
EXPOSE 8002
EXPOSE 8004
EXPOSE 8100
EXPOSE 11625
EXPOSE 11626
Expand Down
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ $ curl http://localhost:8000/friendbot?addr=G...

_Note: In local mode a local friendbot is running. In testnet and futurenet modes requests to the local `:8000/friendbot` endpoint will be proxied to the friendbot deployments for the respective network._

### Readiness Endpoint

The quickstart image provides a `/health` endpoint that indicates when all services are fully ready for use. This endpoint reports HTTP 200 when the image is ready and HTTP 503 when services are still starting up or experiencing issues.

Example usage:

```bash
$ curl http://localhost:8000/health
```

Example response when ready:
```json
{
"status": "ready",
"services": {
"stellar-core": "ready",
"horizon": "ready",
"horizon_health": {
"database_connected": true,
"core_up": true,
"core_synced": true
},
"stellar-rpc": "ready"
}
}
```

The endpoint automatically detects which services are running and only reports "ready" when all detected services are functioning properly. This eliminates the need to write custom scripts to test multiple service endpoints individually.

_Note: The `/health` endpoint provides comprehensive readiness status for all detected services, replacing Horizon's built-in `/health` endpoint with expanded functionality._

### Using in GitHub Actions

The quickstart image can be run in GitHub Actions workflows using the provided action. This is useful for testing smart contracts, running integration tests, or any other CI/CD workflows that need a Stellar network.
Expand Down Expand Up @@ -307,9 +338,9 @@ Managing UIDs between a docker container and a host volume can be complicated. A

The image exposes one main port through which services provide their APIs:

| Port | Service | Description |
| ---- | ------------------------------- | -------------- |
| 8000 | lab, horizon, stellar-rpc, friendbot | main http port |
| Port | Service | Description |
| ---- | ------------------------------------------ | -------------- |
| 8000 | lab, horizon, stellar-rpc, friendbot, health | main http port |

The image also exposes a few other ports that most developers do not need, but area available:

Expand All @@ -318,6 +349,7 @@ The image also exposes a few other ports that most developers do not need, but a
| 5432 | postgresql | database access port |
| 6060 | horizon | admin port |
| 6061 | stellar-rpc | admin port |
| 8004 | health service | internal port |
| 11625 | stellar-core | peer node port |
| 11626 | stellar-core | main http port |
| 11725 | stellar-core (horizon) | peer node port |
Expand Down
6 changes: 6 additions & 0 deletions common/nginx/etc/conf.d/health.conf
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;
}
170 changes: 170 additions & 0 deletions common/readiness/bin/health-service.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there are three implementations. What are the other implementations for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they python script is the main readiness endpoint. AI generated the other two as a backup for different environments.We dont need them I will remove them from the codebase

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"
}
Loading
Loading