mig — message inspector general
A live TUI for inspecting NATS message flow. Point it at a subject (Core or JetStream), see rates, volumes, last-message timing, top subjects, JSON health, and an inferred schema sketch with numeric histograms. The layout adapts: paginated in small terminals, a tiled grid in large ones.
╭─ Total ─╮ ╭─ Rate ──────────╮ ╭─ Last seen ─────────────╮
│ 1,234 │ │ ↑ 12.5 msg/s │ │ 0.3s ago evt.foo 18B │
│ 56.7KB │ │ 60s avg 8.4 │ │ {"id":1,"v":42} │
╰─────────╯ ╰─────────────────╯ ╰─────────────────────────╯
╭─ Msgs/s ─────────────────────────────────────────────────╮
│ ▁▂▂▃▄▄▆▇█▇▇▇▆▅▅▆▇▇█▇▆▆▅▄▄▃▃▂▁▁▁▁▁▂▃▄▄▅▆▆▇█▇▆▅ │
│ peak 47/s · window 120s │
╰──────────────────────────────────────────────────────────╯
Install
Pre-built, signed binaries (Linux/macOS/Windows × x86_64/arm64) are on the
releases page. Each release ships
SBOMs and a cosign-signed checksums.txt.bundle; verification recipe is in
CONTRIBUTING.md.
If you have module access (this repository is private), go install also
works:
GOPRIVATE=github.com/panevain/* go install github.com/panevain/mig/cmd/mig@latest
Or build from source:
git clone https://github.com/panevain/mig
cd mig
make build
./mig --help
Usage
Point at a subject; mig auto-detects whether the server has JetStream and which stream covers your filter:
# Local NATS, all subjects
mig
# Specific subject filter
mig -S 'evt.>'
# Multiple filters, explicit server
mig -s nats://nats.example:4222 -S 'orders.*' -S 'payments.*'
# Force Core NATS (no JetStream)
mig --core -S '>'
# Pin to a specific stream and replay everything
mig --stream EVENTS -S 'evt.>' --all
# Replay last 5 minutes
mig --stream EVENTS --since 5m
# Pipe-friendly mode: no TUI, one JSON message per line on stdout
mig --no-tui -S 'evt.>' | jq .
Benching locally
End-to-end bench against real NATS in two commands:
docker compose --profile bench up -d --build # NATS + BENCH stream + publisher
./mig # consume, watch the tiles light up
The bench profile starts three services: nats (JetStream-enabled server),
nats-init (one-shot stream creator), and nats-pub (the cmd/migbench
publisher, built in-tree via cmd/migbench/Dockerfile).
It emits realistic JSON with rotating event types, randomized
user/region/currency/tags, and incrementing ids — so JSON Health tile shows
~100% parseable, the schema sketch fills out, and Top subjects spreads across
bench.x.0..bench.x.9. Tune with env vars before bringing the stack up:
BENCH_RATE=10000 BENCH_SUBJECTS=50 docker compose --profile bench up -d --build
For a faster dev loop without rebuilding the container on every code change,
run the publisher natively against a NATS-only stack:
docker compose up -d # NATS only (no init, no pub)
make bench-pub # ./cmd/migbench against localhost:4222
Tear down with docker compose --profile bench down -v (the -v wipes JS
state). Monitoring is on localhost:8222 (e.g., curl localhost:8222/varz).
On Linux hosts where Docker's bridge networking is flaky (Fedora / nftables /
firewalld in particular), copy compose.override.yaml.example to
compose.override.yaml to switch every bench service to host networking.
Linux-only — leave it as .example on macOS/Windows.
Auth
# JWT + nkey
mig --creds ~/.nats/user.creds
# nkey only
mig --nkey ~/.nats/user.nk
# Username + password (also picks up NATS_PASSWORD env)
mig --user observer --password $TOKEN
# TLS
mig --tlscert client.crt --tlskey client.key --tlsca ca.crt
# Existing nats CLI context (opt-in)
mig --context production
Tiles
| Tile |
What you see |
| Header |
Connection status dot, server URL, source kind (core/jetstream + stream), uptime, drop count, recent error count |
| Total |
Lifetime message count + total bytes |
| Rate |
Current msg/s (5s EMA) + 60s EMA + jitter stddev. Up/down/flat trend arrow |
| Msgs/s sparkline |
120-second history of msg/s with peak marker |
| Bytes |
Bytes/s (5s EMA) + 60s avg + small inline byte sparkline |
| Last seen |
Age of last message (red when older than --stale), subject, JSON-pretty preview |
| Top subjects |
Up to 10 busiest subjects with counts and last-seen ages |
| JSON health |
% parseable, ok/bad counts, count of valid-but-non-object payloads |
| Schema sketch |
Inferred JSON paths with types, numeric range/μ/p50 + histogram, string distinct count + example |
| Drops |
Red banner only visible when slow-consumer drops have occurred |
Layout
mig adapts to terminal size automatically:
| Width × Height |
Layout |
< 40 × 10 |
"resize me" message |
< 60 wide or < 16 tall |
One tile per page; tab / arrows cycle |
60–119 × 16–29 |
2-column grid, 2 pages |
120–179 × ≥ 30 |
3-column grid, all tiles visible |
≥ 180 |
4-column grid, big sparkline, expanded subjects/schema |
Keys
| Key |
Action |
q, Ctrl-C |
Quit |
tab, →, l |
Next page (paginated layouts) |
shift-tab, ←, h |
Previous page |
r |
Reset all stats and schema (uptime preserved) |
p |
Pause UI rendering (data still recorded) |
s |
Open the settings overlay (color scheme picker) |
? |
Toggle the keyboard help overlay |
Settings & themes
mig ships three color schemes — default, solarized-dark, high-contrast
— and remembers your choice and a few behavior knobs across runs.
# List available themes
mig --theme=?
# Pick at startup
mig --theme solarized-dark
# Adjust the rate sparkline window (in seconds, default 120, range 8..600)
mig --spark-seconds 300
# Widen the histogram reservoir (per-path numeric samples, default 256, range 16..4096)
mig --hist-samples 1024
Inside the TUI, press s to open the settings overlay. The overlay has two
sections:
- Theme —
↑/↓ moves between themes, enter applies the highlighted
one. Live update.
- Behavior — refresh interval, stale threshold (red marker on the
Last seen tile), rate sparkline window, and histogram reservoir size
(per-path numeric samples feeding min/max/p50/p95/p99 and the histogram).
←/→ cycles each value through preset steps; enter advances forward.
Refresh and stale take effect immediately; the spark window and histogram
reservoir apply on restart (their per-path rings are sized at construction).
Every change writes the full settings file to disk immediately; close the
overlay with s or esc.
The persisted file is JSON; resolution order:
--config PATH (CLI flag)
$MIG_CONFIG (environment)
$XDG_CONFIG_HOME/mig/config.json (or ~/.config/mig/config.json if
XDG_CONFIG_HOME is unset)
Precedence at startup is flag > config file > built-in default, so any
flag on the command line always wins over whatever was last saved. The
on-disk schema is intentionally small and additive — new settings appear
as new keys without breaking existing files. Stale or invalid file values
are warned and ignored rather than blocking startup.
Example config.json:
{
"theme": "solarized-dark",
"refresh": "200ms",
"stale": "5s",
"spark_seconds": 120,
"hist_samples": 256
}
Reliability
- Cold-start with NATS down: mig exits immediately with
cannot reach NATS at <url> instead of hanging or surfacing an opaque buffer error. Mid-session drops reconnect forever; header turns red until the connection is back.
- JetStream auto-detect: if the stream is missing, mig surfaces the available stream list and suggests
--core or an explicit --stream. JS-disabled servers fall back to Core silently.
- Slow-consumer drops increment a counter and pop the Drops tile/banner.
- Malformed JSON is counted (
bad=N) but never crashes anything; the LastSeen tile still renders the raw bytes.
- Subject and schema-path tracking are bounded (
--max-subjects, --max-paths) with eviction so high-cardinality streams don't OOM mig.
Flag reference
mig --help
All flags: see mig --help. Highlights:
--refresh DURATION (default 200ms, min 50ms) — UI tick interval
--stale DURATION (default 5s) — red threshold on last-seen
--max-subjects N (default 256) — bounded top-N subject map
--max-paths N (default 512) — bounded schema path tracker
--schema-sample F (default 1.0) — fraction of valid JSON fed into the schema engine
--no-tui — flat one-line-per-message stdout for piping/grepping
--json-pretty — pretty-print JSON in --no-tui mode
--theme NAME — color scheme; --theme=? lists names (default: default)
--config PATH — user config file path; defaults to $MIG_CONFIG or
$XDG_CONFIG_HOME/mig/config.json
--spark-seconds N (default 120, range 8..600) — width of the rate
sparkline ring
--hist-samples N (default 256, range 16..4096) — per-path numeric
reservoir feeding min/max/quantiles and the histogram. Larger = more
recent history retained at the cost of memory (~N × 8B × max-paths)
Contributing
Contributions welcome — see CONTRIBUTING.md. For security
issues, please use private reporting per SECURITY.md.
License
MIT