Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions images/go_chains_hc/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
## Connected Chains Healthcheck

A minimal Go binary/container that exposes a `/readyz` HTTP endpoint for blockchain node readiness probes. Designed to be used alongside any compatible node (Bitcoin, Dogecoin, Litecoin, etc.) in a Kubernetes pod.
A minimal Go binary/container that exposes a `/readyz` HTTP endpoint for blockchain node readiness probes. Designed to be used alongside any compatible node in a Kubernetes pod or as standalone Docker service.

### How it works

On each `/readyz` request, the sidecar runs a configurable set of checks against the node's JSON-RPC API. It returns 200 OK when all checks pass, or an error with a message on the first failing check.

### Checks

#### Bitcoin / Dogecoin

| Check | RPC Method | Description |
|---|---|---|
| `blockdownload` | `getblockchaininfo` | Passes when `initialblockdownload` is `false`. Default for all chains. |
| `blockdownload` | `getblockchaininfo` | Passes when `initialblockdownload` is `false` |
| `txindex` | `getindexinfo` | Passes when `txindex.synced` is `true`. |
| `connectioncount` | `getconnectioncount` | Passes when the node has at least `MIN_CONNECTIONS` peers. |

#### XRPL

| Check | RPC Method | Description |
|---|---|---|
| `serverstatus` | `ping` | Passes when `status` is `success` |
| `nodesynced` | `server_info` | Passes when `server_state` is `full`\|`proposing`\|`validating`. |
| `peercount` | `server_info` | Passes when the node has at least `MIN_CONNECTIONS` peers. |

### Environment Variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `NODE_URL` | yes | — | RPC endpoint, e.g. `http://localhost:8332` |
| `NODE_USER` | no | — | RPC auth username |
| `NODE_PASS` | no | — | RPC auth password |
| `CHECKS` | no | `blockdownload` | Comma-separated list of checks to run (`blockdownload, txindex, connectioncount`) |
| `CHECKS` | yes | - | Comma-separated list of checks to run (Check above section) |
| `MIN_CONNECTIONS` | no | `8` | Minimum peer connections required for the `connectioncount` check |
| `DEBUG` | no | `false` | Enable debug logs |

Expand Down
77 changes: 77 additions & 0 deletions images/go_chains_hc/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ type Check func(ctx context.Context, client *http.Client, cfg Config) error
const jsonRPCVersion = "2.0"

var registry = map[string]Check{
// BTC/Dogecoin
"blockdownload": checkBlockDownload,
"txindex": checkTxIndex,
"connectioncount": checkConnectionCount,

// Ripple
"nodesynced": checkNodeServerState,
"peercount": checkPeerCount,
"serverstatus": checkServerStatus,
}

type rpcRequest struct {
Expand Down Expand Up @@ -139,3 +145,74 @@ func checkConnectionCount(ctx context.Context, client *http.Client, cfg Config)

return nil
}

func checkServerStatus(ctx context.Context, client *http.Client, cfg Config) error {
result, err := doRPC(ctx, client, cfg, "ping")
if err != nil {
return err
}

var info struct {
Status string `json:"status"`
}
if err := json.Unmarshal(result, &info); err != nil {
return fmt.Errorf("parse ping response: %w", err)
}
if info.Status != "success" {
return fmt.Errorf("unexpected status: %q", info.Status)
}

return nil
}

func checkNodeServerState(ctx context.Context, client *http.Client, cfg Config) error {
result, err := doRPC(ctx, client, cfg, "server_info")
if err != nil {
return err
}

var info struct {
State struct {
ServerState string `json:"server_state"`
} `json:"info"`
}

if err := json.Unmarshal(result, &info); err != nil {
return fmt.Errorf("parse server_state response: %w", err)
}

validServerStates := map[string]bool{
"full": true,
"validating": true,
"proposing": true,
}

if !validServerStates[info.State.ServerState] {
return fmt.Errorf("unexpected server_state: %q", info.State.ServerState)
}

return nil
}

func checkPeerCount(ctx context.Context, client *http.Client, cfg Config) error {
result, err := doRPC(ctx, client, cfg, "server_info")
if err != nil {
return err
}

var info struct {
State struct {
Peers int `json:"peers"`
} `json:"info"`
}

if err := json.Unmarshal(result, &info); err != nil {
return fmt.Errorf("parse server_state response: %w", err)
}

if info.State.Peers < cfg.MinConnections {
return fmt.Errorf("not enough peers: %d < %d", info.State.Peers, cfg.MinConnections)
}

return nil
}
6 changes: 3 additions & 3 deletions images/go_chains_hc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ func configFromEnv() (Config, error) {
return Config{}, fmt.Errorf("NODE_URL is required")
}

checks := []string{"blockdownload"}
if raw := os.Getenv("CHECKS"); raw != "" {
checks = strings.Split(raw, ",")
checks := strings.Split(os.Getenv("CHECKS"), ",")
if len(checks) == 0 || checks[0] == "" {
return Config{}, fmt.Errorf("CHECKS is required")
}

for _, name := range checks {
Expand Down
Loading