- 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>
- ping_batch() pings each peer's public IP in parallel from the host
(system ping, 1 packet, 1 s timeout, private IPs skipped)
- New "Ping" column next to stellar-core's "Latency" in the -p/-g table;
"—" when the peer filters ICMP
- Low ping with high latency hints at a VPN exit / relay in front of
the actual node
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fetch Horizon via curl inside the container (127.0.0.1:8000) instead of
the host port 31401; the host->bridge forwarding hung until the 5 s
timeout and made each cycle take ~6 s instead of ~1 s
- Report an error when Horizon's core_latest_ledger is 0 (no connection
to stellar-core) instead of a false "Synced" with a negative lag
- Compute the ingest lag against stellar-core's own ledger
- "History Oldest" now shows history_elder_ledger; the latest ledger is
listed separately as "History Latest"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Optional JSON config ($PI_STAT_CONFIG, ~/.config/pi-stat/config.json,
<script dir>/pi-stat.json) with refresh_interval; -i SEC overrides it
- Refresh deadline anchored to cycle start; warn when a cycle takes
>= 80% of the refresh interval or exceeds it
- Switch the tty to cbreak once per session so keypresses made during a
long cycle are not dropped
- Ship pi-stat.json with the repo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:05:04 +00:00
3 changed files with 264 additions and 55 deletions
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.:
```json
{
"_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).
@@ -27,10 +45,11 @@ Single self-contained script (`pi-stat`). Logic is split into helper functions a
- **Docker SDK** — container CPU/RAM stats; all commands run inside the auto-detected container (`mainnet` → `testnet2` → `testnet`, first running one) via `exec_run`
- **Horizon API** — `curl http://127.0.0.1:8000/` run inside the container via `exec_json()`, not via the host port `31401` (host→bridge forwarding can hang until timeout if Docker's host firewall rules are broken)
- **PostgreSQL** — `psql` inside the container; used for peer DB counts and pubkey→IP mapping
- **ZFS CLI** — `zpool`/`zfs` commands on the host for disk health (`z01pool`); if `zpool` is not found (`HAS_ZFS = False`), falls back to `shutil.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 as `Ping` next to stellar-core's `Latency` in the `-p`/`-g` table; TCP values are marked with a trailing `t` (legend printed below the table), `—` if neither answers
### Output sections (in order)
@@ -49,11 +68,17 @@ Single self-contained script (`pi-stat`). Logic is split into helper functions a
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_addr` from PostgreSQL)
- **Validator matching** — `load_validators()` parses `stellar-core.cfg` and indexes by IP, name, and public key; matched peers show a truncated key + `(Vn)` label
- **Ping vs. Latency** — `Latency` is stellar-core's round trip to the peer *process*, `Ping` is 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 with `Latency > LATENCY_PING_RATIO (3) × Ping` get 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 `⚡` or `t` marker is present
- **Geo caching** — `_geo_cache` is a module-level dict; `geo_batch()` only fetches IPs not already cached
- **Flag parsing** — manual `sys.argv` parsing; `-g` implies `-p`; `-i` accepts an optional next argument as the refresh interval
- **Config precedence** — `config_refresh()` reads the config value; an explicit `-i SEC` argument overrides it
- **Cycle timing** — `interactive_loop()` measures cycle duration and warns via `warn()` when it nears or exceeds `refresh_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 overruns `refresh_interval`
"_comment":"refresh_interval in Sekunden, min. 1 (Default: 5)",
"refresh_interval":10
}
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.