1 Investigate
Mathew Storm edited this page 2026-07-19 14:38:20 -04:00

Investigate

stormpulse investigate runs one-shot diagnostics that end in a verdict, not a log dump. Each investigation checks a fixed set of suspects and prints a case file: what was ruled out, what the evidence points at, and what could not be checked - with the exact command that would supply the missing evidence.

$ stormpulse investigate
Investigations (run: stormpulse investigate <name>):
  flaps           agent websocket reconnect churn
  box             host starvation, reboots, upgrades, kernel faults
  logs-pipeline   per-group shipping health and parser drift
  garage health   garage daemon restarts and maintenance load  (run: stormpulse garage investigate health)

Reading a case file

CASE FILE: flaps - agent websocket reconnect churn
  Window:  2026-07-19 06:00 → now
  Receipt: earned 2026-07-19: the alpha flap storm ...

VERDICTS
  IMPLICATED    reconnect churn
                14 drops: 8 x keepalive timeout (pings unanswered 20s), ...
  CLEARED       refresh storm
                0 garage_refresh results in window (2 commands total).
  IMPLICATED    process/box freeze
                5 log lines reached journald >= 15s late; worst 48s at 2026-07-19T10:19:24.
  INCONCLUSIVE  kernel faults
                System journal not readable as this user.
                run: sudo journalctl -k --since "2026-07-19 06:00" ...

Three verdicts, always:

  • CLEARED - checked and ruled out, with the measurement that rules it out. A cleared suspect is a finding, not the absence of one.
  • IMPLICATED - the evidence points here. One check rarely proves a root cause; it tells you where to dig next.
  • INCONCLUSIVE - the check could not see. The run: line is the command that would supply the evidence. An investigation never escalates privileges itself, and an unwitnessed window is never reported as healthy.

After the verdicts: next moves (what to run next, in order) and open questions (things only you can answer, like "was that reboot yours?").

Windows

All investigations accept a time window:

stormpulse investigate flaps --since 6h
stormpulse investigate box --since "2026-07-19 06:00" --until "2026-07-19 07:10"

--since/--until take 90m / 24h / 7d or YYYY-MM-DD [HH:MM]. Default: the last 24 hours.

The investigations

flaps - why the agent keeps reconnecting. Tallies drops by error family (keepalive timeout = the peer, or this process's own starved event loop, stopped answering pings for 20s; handshake timeout = the backend couldn't accept within 10s; abrupt TCP drop = a process died or restarted). Rules the refresh path and log shipping in or out. Its sharpest check compares each log line's own timestamp against journald's receipt time: a large gap means the process or the whole box was frozen, a signature that shows up even when nothing else errors.

box - what starved the host. Point-in-time CPU steal and iowait, reboots in the window checked against unattended-upgrades' scheduled reboots (a boot at the scheduled time is routine, not an anomaly), package-manager activity correlated to the window, and kernel rcu/stall/hung/oom lines. A clean kernel log does not acquit the hypervisor: a full host-side pause leaves no in-guest trace except the freeze signature above.

logs-pipeline - per log group: is shipping healthy, and when a group ships nothing but drops everything, is that a source whose format drifted or noise the parser suppresses on purpose? It decides by feeding a raw sample from the source through the group's own parser. Two numbers that mislead if read naively: a duration_ms pinned near ship_interval x 0.9 is the drain window working as designed, and dropped includes deliberately suppressed lines, not just unparseable ones.

garage health - declared by the Garage integration. Detects daemon shutdown waves (many workers exiting within seconds = Garage went down; someone sent SIGTERM) and maintenance load (scrub, resync, snapshot, compaction) inside the window.

For integration authors

An integration declares investigations on its contract (investigations=(InvestigationSpec(name, title, run),)). The runner receives the parsed integration config and the resolved window, and returns a CaseFile built from stormpulse.sdk.investigate types; the CLI owns rendering. Investigations are read-only by contract: observe and report, never mutate. See the Architecture page and ADR CORE-005 (decision 14) in the repo.