iowatchd 0.5.2

Monitors disk IO usage and logs which processes are consuming it
# iowatchd

Monitors disk IO usage (`%util`, the same as `iostat -x`) and, when it goes above 80%, logs the processes doing the most reads/writes.

- Pure Rust, no dependencies: ~360 KB binary, ~2.5 MB of RAM, ~0% CPU.
- Reads only `/proc/diskstats` every 2 s; the `/proc/<pid>/io` scan happens only during an alert.
- Logs to journald and to `/var/log/iowatchd.log` (rotated at 5 MB).
- Boot service picked automatically: systemd (tested), OpenRC and runit (untested). On other init systems, `install.sh` installs only the binary and prints the command to run.
- Automatic disk detection (`device = auto`): the physical disk that holds `/`, even with partitions, LVM or LUKS (SATA, NVMe, etc.).
- Under systemd: `Nice=19`, idle IO class, `CPUQuota=5%`, `MemoryMax=20M`.

## Installation

From source (detects the init system, installs and starts the service):

    ./install.sh
    ./install.sh uninstall      # keeps /etc/iowatchd.conf and the log

Or with make (`PREFIX`, `DESTDIR`, `SYSTEMDUNITDIR` are honoured):

    make && sudo make install

Prebuilt static binaries are on the GitHub releases page; `cargo install iowatchd` builds from crates.io, and `cargo binstall iowatchd` downloads the prebuilt binary.

Packaging recipes are in `packaging/`: Arch (`PKGBUILD`, and `arch-bin/` for the prebuilt variant), Homebrew (`homebrew/iowatchd.rb`, for a personal tap), RPM (`iowatchd.spec`) and Debian (`debian/`; copy it to the source root and run `dpkg-buildpackage -us -uc`). The RPM and Debian recipes are untested.

If the old installation (`ssd-io-monitor`) exists, `install.sh` removes it. Man pages (`iowatchd(8)`, `iowatchd.conf(5)`, `iowatchd-log(1)`, `iowatchd-notify(1)`), bash/zsh/fish completions and a logrotate file are installed too.

## Usage

    iowatchd --help                 options
    iowatchd --once                 one measurement, then exit
    iowatchd --once --json          same, as a JSON line
    iowatchd --list-devices         list disks (marks the auto choice)
    iowatchd --report --since 7d    who caused the most alerts + diagnosis (needs log_file)
    iowatchd --check-config         validate and print the configuration
    iowatchd -d nvme0n1 -t 60       override disk and threshold
    man iowatchd                    full manual

Options (`-c -d -L -R -t -i -n -j -o -C -f -q -v -h -V`; short flags can be grouped, e.g. `-jq`) take precedence over the configuration file. Exit codes: 0 success, 1 runtime error, 2 usage error. `SIGHUP` reloads the configuration (`sudo systemctl reload iowatchd`); `SIGUSR1` logs the current status; `SIGTERM`/`SIGINT` stop it cleanly. The config path can come from `$IOWATCHD_CONFIG`, and `/etc/iowatchd.conf.d/*.conf` drop-ins override the main file. Under systemd the unit is `Type=notify`.

Log shortcut: `iowatchd-log` (latest alerts), `iowatchd-log -f` (follow), `iowatchd-log -s` (status), `-n 50`, `--since '1 hour ago'`, `-j` (JSON). Prebuilt static binaries are attached to each GitHub release.

Equivalent commands:

    systemctl status iowatchd
    journalctl -u iowatchd -f
    tail -f /var/log/iowatchd.log

Example log entries:

    ALERT IO=94% on sda (await=12.3ms queue=3.1 read=0KB/s write=177.4MB/s psi=8.0%) | 1) dd pid=29847 user=thiago unit=app-konsole-1521 R=0KB/s W=177.4MB/s cmd="dd if=..." | 2) ...
    IO back to normal: 0% on sda

## Report and diagnosis

`iowatchd --report` summarizes the log (default: last 24 h; `--since 7d`, `--json` for machines) and tells **whether the disk is busy or sick**:

- **Slow, not busy**: alerts with high latency (`await` ≥ 200 ms) and almost no traffic (< 1 MB/s) point to a degrading disk/SSD, not to a process. The processes listed in such alerts are victims waiting for the disk.
- **Intermittent stalls**: bursts with high throughput and low latency show the disk is fast when it works.
- **SMART** (optional): with `smartctl` (smartmontools) and root, it also flags reallocated, pending and uncorrectable sectors. Without them the section says `unavailable`. Disable it with `smart = off`.

Example on a failing SSD (`sudo iowatchd --report`):

    iowatchd report: alerts in the last 1d (from /var/log/iowatchd.log)
    alerts: 66   peak IO: 100%   first: 2026-10-17 12:13:45   last: 2026-10-17 20:49:24

    Diagnosis:
      - 47 of 66 alerts (71%) show high latency (await >= 200 ms) with almost no traffic (< 1 MB/s): the disk is slow, not busy. This points to a degrading disk/SSD (bad cells, retries) rather than to any process.
      - await averaged 968 ms (peak 2018 ms); IO pressure (psi) peaked at 99%, so tasks were really blocked.
      - 1 alert(s) had high throughput with low latency: the disk is fast when it works, so the problem is intermittent stalls.
      - Processes listed with only a few KB/s are victims waiting for the disk, not the cause.

    === SMART (sda) ===
      WARNING: the disk reports sector problems:
          5 Reallocated_Sector_Ct = 2
          197 Current_Pending_Sector = 2
      Back up your data now and consider replacing the disk. ...

    Top offenders (by number of alerts they appeared in):
       #  process              unit                     alerts     avg IO    peak IO
       1  khugepaged           -                            20    210KB/s    255KB/s
       2  MainThread           app-org.kde.konsole-2189     15    2.6MB/s   37.6MB/s

On a healthy disk with a real heavy writer the diagnosis section is omitted and the top offenders are the cause. Without root the SMART section reads:

    === SMART (sda) ===
      unavailable (could not read SMART data; try: sudo iowatchd --report ...)

## Configuration

`/etc/iowatchd.conf` (template in `config.conf`, documented in `man iowatchd.conf`). After editing: `sudo systemctl reload iowatchd`. Set `log_format = json` for JSON lines, or `log_max_mb = 0` to let logrotate manage the file.

The alert header explains the `%` figure: `await` (ms per request), `queue` (requests in flight), disk throughput and `psi` (share of the last 10 s in which tasks stalled waiting for IO, from `/proc/pressure/io`). Each process shows its systemd `unit` or container.

### Notifications

Set `notify_command` to run any command on every alert and recovery. It receives `IOWATCHD_EVENT` (`alert`/`normal`), `IOWATCHD_DEVICE`, `IOWATCHD_UTIL` and `IOWATCHD_MESSAGE`:

    notify_command = logger -t iowatchd "$IOWATCHD_MESSAGE"

For desktop pop-ups use the bundled helper: `notify_command = iowatchd-notify` (shows the top processes on every logged-in desktop session, throttled to one alert per minute).

## Development

    cargo fmt --check && cargo clippy --all-targets && cargo test

Release packaging is checked in CI (Arch, Homebrew, `.deb`, `.rpm`, static tarballs). Locally, `scripts/check-versions.sh` verifies that every file carrying the version agrees with `Cargo.toml` (pass `vX.Y.Z` to also compare a tag).

## License

MIT. See [LICENSE](LICENSE).