Files
pi-stat/CLAUDE.md
T
olieske 20740b3704 feat: TCP connect fallback for ping, flag high latency vs. ping
- 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>
2026-09-23 08:40:31 +00:00

85 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
./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.:
```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).
### Data sources
- **Docker SDK** — container CPU/RAM stats; all commands run inside the auto-detected container (`mainnet` → `testnet2` → `testnet`, first running one) via `exec_run`
- **Stellar Core HTTP API** — `exec_json()` runs `stellar-core http-command <cmd>` inside the container
- **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)
1. Container stats — Docker CPU/RAM, Horizon + Core versions (skipped in `-p`/`-g` mode)
2. Disk — ZFS (`z01pool`) if `zpool` is available, otherwise `shutil.disk_usage()` (skipped in `-p`/`-g` mode)
3. Protocol status — Stellar Core state, ledger, quorum phase
4. API status — Horizon ingest lag
5. 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_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`