Live response-time stats from streamed AppPack web logs.
apppack-stats shells out to apppack logs --raw --follow for the app you
name, parses the JSON request lines, and renders a live Textual data
table grouped by (method, normalized path). Click any column header to
re-sort by it; click again to flip the direction.
Slowest endpoints — 1842/1903 lines parsed, 47s elapsed
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━┓
┃ Method ┃ Path ┃ Count ┃ Avg ms ┃ p95 ms ┃ Max ms ┃ Err ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━┩
│ GET │ /reports/<id>/export │ 7 │ 1842 │ 2210 │ 2480 │ │
│ POST │ /api/v1/orders │ 23 │ 412 │ 711 │ 980 │ 2 │
│ GET │ /dashboard │ 91 │ 188 │ 320 │ 640 │ │
└────────┴───────────────────────────────┴───────┴────────┴────────┴────────┴─────┘
No install required — run it straight from PyPI with
uv:
uvx apppack-stats <appname>Press q (or Ctrl+C) to stop.
| Key | Action |
|---|---|
| Click column header | Sort by that column (click again to reverse) |
n |
Toggle URL normalization on/off |
q |
Quit |
The AppPack CLI must be on
your PATH and authenticated against the account that owns the app.
| Flag | Default | Notes |
|---|---|---|
--refresh SEC |
1.0 |
Seconds between redraws |
--start DUR |
5m |
How far back to seed history (30m, 2h, 1d, …) |
--prefix STRING |
none | AppPack log-group prefix filter — see note below |
--no-normalize |
off | Group by raw path instead of normalizing IDs |
-o, --output PATH |
none | On exit, write the final stats to PATH as CSV (- for stdout). Rows are sorted by whichever column was active in the UI when you quit. |
The table fills the terminal and scrolls when there are more rows than fit.
apppack logs --prefix filters on AppPack's underlying CloudWatch
log-group names, which don't always begin with the service name shown in
the rendered (web/web/<task>) label. Different AppPack apps use
different log-group naming conventions, so a fixed default like web
silently drops everything for some apps. By default apppack-stats
passes no prefix and lets the parser filter — only access-log JSON lines
(those with method, path, status, response_time_us) are counted;
everything else is skipped.
By default, paths are normalized so that /orders/12345 and /orders/67890
share a row as /orders/<id>. UUIDs become /<uuid>, long hex hashes
become /<hash>. Pass --no-normalize to keep raw paths.
Access-log shapes live in
src/apppack_stats/extractors.py.
Append a new LogShape(...) to the SHAPES list — pick a time_field
key that doesn't collide with existing ones, set time_unit to "us",
"ms", or "s", and the parser will pick it up automatically.