- If a peer filters ICMP, measure the TCP connect time to its peer port instead; such values are marked with a trailing "t" - Show ⚡ between Latency and Ping when stellar-core's latency exceeds the ping/TCP RTT by more than LATENCY_PING_RATIO (3), hinting at a VPN exit / relay in front of the actual node - Legend below the peer table, only for markers actually shown Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What This Is
pi-stat is a fast status monitoring tool for Stellar Core nodes on the Pi Network. It provides quick system diagnostics as an alternative to the slow pi-node status command.
Running
./pi-stat # Standard report
./pi-stat -p / --peers # Extended peer table
./pi-stat -g / --geo # With geolocation (implies -p)
./pi-stat -s / --show-ip # Show IP column (hidden by default)
./pi-stat -x / --extended # Show full RX/TX/RX-rate/TX-rate columns (default: ratio only)
./pi-stat -i [SEC] / --interactive [SEC] # Interactive auto-refresh (default: 5s, configurable)
No build step. Requires pip install docker and a running Docker daemon. Runs on Linux, WSL2, and Windows.
Configuration
Optional JSON config file, first match wins: $PI_STAT_CONFIG → ~/.config/pi-stat/config.json → <script dir>/pi-stat.json (see pi-stat.json, shipped with the repo). Loaded by load_config(); missing, unreadable or non-object files fall back to defaults with a warning on stderr.
| Key | Default | Meaning |
|---|---|---|
refresh_interval |
5 |
Interactive auto-refresh interval in seconds (clamped to >= 1) |
Precedence for the refresh interval: -i SEC on the command line > config file > DEFAULT_REFRESH (5).
Standard JSON has no comment syntax; document settings with a _comment key instead (any key not read by config_refresh() is ignored), e.g.:
{
"_comment": "refresh_interval in seconds, min. 1 (default: 5)",
"refresh_interval": 5
}
Architecture
Single self-contained script (pi-stat). Logic is split into helper functions and a run_cycle() function that collects data and renders one full screen, called either once (normal mode) or in a loop (interactive mode).
Data sources
- Docker SDK — container CPU/RAM stats; all commands run inside the auto-detected container (
mainnet→testnet2→testnet, first running one) viaexec_run - Stellar Core HTTP API —
exec_json()runsstellar-core http-command <cmd>inside the container - Horizon API —
curl http://127.0.0.1:8000/run inside the container viaexec_json(), not via the host port31401(host→bridge forwarding can hang until timeout if Docker's host firewall rules are broken) - PostgreSQL —
psqlinside the container; used for peer DB counts and pubkey→IP mapping - ZFS CLI —
zpool/zfscommands on the host for disk health (z01pool); ifzpoolis not found (HAS_ZFS = False), falls back toshutil.disk_usage()— works on Linux without ZFS, WSL2, and Windows - ip-api.com — batch geolocation (Country, City), cached in
_geo_cache; only fetched with-g - System
ping/ TCP connect — per peer from the host (ping_batch(), parallel, private IPs skipped): one ICMP ping (1 s timeout); if the peer filters ICMP, falls back to a TCP connect on the peer port from its address (1 s timeout). Shown asPingnext to stellar-core'sLatencyin the-p/-gtable; TCP values are marked with a trailingt(legend printed below the table),—if neither answers
Output sections (in order)
- Container stats — Docker CPU/RAM, Horizon + Core versions (skipped in
-p/-gmode) - Disk — ZFS (
z01pool) ifzpoolis available, otherwiseshutil.disk_usage()(skipped in-p/-gmode) - Protocol status — Stellar Core state, ledger, quorum phase
- API status — Horizon ingest lag
- Peer connections — compact counts, or full table with
-p/-g
Interactive mode (-i)
interactive_loop() renders via run_cycle() into a StringIO buffer, writes it atomically to avoid flicker, then waits for keypresses using select in setcbreak mode:
d— drop a single outbound peer (do_drop_workflow)a— drop all outbound peers (drop_all_workflow)q/ Ctrl+C — clean exit with terminal state restore
Outbound peers are numbered in the table. Full node IDs are fetched via peers?fullkeys=true for the droppeer API call.
The refresh deadline is anchored to the start of the cycle (cycle_start), not its end, so the update's own execution time doesn't stack on top of refresh_interval. Execution time is measured and a warning is printed (into the same buffered screen) when a cycle takes >= REFRESH_WARN_RATIO (80%) of the interval, or exceeds it outright.
Key patterns
- Graceful degradation — every external call is wrapped in try-except; missing data renders as
— - Dynamic column widths — all table columns sized at runtime from actual data length
- NAT detection — inbound peers with private IPs are replaced with their DB-stored public IP (
db_peer_addrfrom PostgreSQL) - Validator matching —
load_validators()parsesstellar-core.cfgand indexes by IP, name, and public key; matched peers show a truncated key +(Vn)label - Ping vs. Latency —
Latencyis stellar-core's round trip to the peer process,Pingis ICMP to the peer's IP; a low ping with high latency hints at a VPN exit / relay in front of the actual node. Peers withLatency > LATENCY_PING_RATIO (3) × Pingget a⚡between the two columns (2-column cell, since⚡is double-width in terminals); a legend line is printed below the table only when a⚡ortmarker is present - Geo caching —
_geo_cacheis a module-level dict;geo_batch()only fetches IPs not already cached - Flag parsing — manual
sys.argvparsing;-gimplies-p;-iaccepts an optional next argument as the refresh interval - Config precedence —
config_refresh()reads the config value; an explicit-i SECargument overrides it - Cycle timing —
interactive_loop()measures cycle duration and warns viawarn()when it nears or exceedsrefresh_interval, so a slow update doesn't silently push out the actual refresh cadence - Persistent cbreak — the tty is switched to cbreak once for the whole session (not reset to canonical between cycles); doing it per-cycle would flush unread input on the mode switch and leave the terminal line-buffered during
run_cycle(), silently dropping keypresses (e.g.q) made while a cycle overrunsrefresh_interval