DiskWatch Capacity forecasting for directory trees Techlosoft - Storage Health Center WHAT IT IS ========== DiskWatch answers one question the rest of the Storage Health Center line does not: "at the rate this is filling up, WHEN do I run out?" You take usage samples over time. DiskWatch stores each one as a timestamped line in an append-only history file. When you ask for a forecast, it fits a least-squares linear regression of bytes against time for every watched path and projects the date the tree crosses a capacity you state. It reports the growth slope, the R-squared goodness-of-fit so you can tell a trustworthy trend from noise, the duration of the sample window, and the projected days until full. Sibling tools cover the other angles: DrivePulse maps folder space and finds the largest files, StorageLens and SpaceMedic inspect and diagnose, DevicePulse tracks snapshot history with growth-percentage warnings. DiskWatch is the only one that predicts. INSTALL ======= Pre-built binaries are in dist/. There is nothing to install - copy the one for your platform anywhere on your PATH and run it. dist/diskwatch-linux-amd64 Linux, x86-64 dist/diskwatch-darwin-arm64 macOS, Apple Silicon dist/diskwatch-darwin-amd64 macOS, Intel dist/diskwatch-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x diskwatch-linux-amd64 To build from source you need Go 1.24 or newer. There are no dependencies of any kind, so no network access is required: go build -o diskwatch . COMMANDS ======== diskwatch sample --watch [--watch ...] --history [--capacity 10GB] [--json] diskwatch forecast --history [--capacity 10GB] [--json] diskwatch history --history [--json] diskwatch help | -h | --help sample Walks each watched tree, sums the apparent size of every regular file, and appends exactly ONE JSON line to the history file. Existing lines are never read back, rewritten or reordered - the history is strictly append-only, so it is safe to run from cron while something else is reading it. forecast Groups the recorded samples by path, fits a least-squares line to each one independently, and reports the trend plus the projected exhaustion date. history Lists the samples recorded so far, newest last. FLAGS --watch Directory tree to measure. Repeat the flag to watch several trees in a single history. Bare positional arguments to `sample` are also treated as watched dirs. --history The JSON-lines history file. Created on first sample, along with any missing parent directories. --capacity The capacity to forecast against. If you omit it on forecast, the most recent capacity recorded in the history is used. --json Machine-readable output (forecast and history). Short forms -w, -f and -c are accepted for --watch, --history and --capacity. Flags may appear before or after positional arguments; either order works. CAPACITY STRINGS ================ 10GB 10 GiB (10737418240 bytes) 500MB 500 MiB (524288000 bytes) 2TiB 2 TiB (2199023255552 bytes) 1.5G 1.5 GiB (1610612736 bytes) 1048576 raw bytes Suffixes are BINARY: 1KB means 1024 bytes, not 1000. KB/MB/GB and KiB/MiB/GiB are treated identically. A bare number is a byte count. Underscores and commas in the number are ignored, so 1_000_000 and 1,000,000 both work. QUICK START =========== # take a sample now, and again later (cron, a timer, or by hand) diskwatch sample --watch /var/log --history usage.jsonl --capacity 20GB diskwatch sample --watch /var/log --history usage.jsonl --capacity 20GB # once you have two or more samples diskwatch forecast --history usage.jsonl # what has been recorded so far diskwatch history --history usage.jsonl # feed a monitoring system diskwatch forecast --history usage.jsonl --json EXAMPLE OUTPUT ============== DiskWatch capacity forecast history : /srv/usage.jsonl (4 samples) capacity : 20.0 GiB (21474836480 bytes, from history) /var/log samples : 4 window : 2026-08-03T02:00:00Z -> 2026-08-10T02:00:00Z (7d, 7.000000 days) latest usage : 12.4 GiB (13314398618 bytes) [62.0% of capacity] growth rate : 640.0 MiB/day (671088640.000 bytes/day) fit R^2 : 0.998412 days to full : 12.160000 (12.16 days) full at : 2026-08-22T05:50:24Z status : projected - projected to reach 20.0 GiB in 12.16 days HOW THE FORECAST IS COMPUTED ============================ For each watched path, every recorded sample becomes a point (x, y) where x is days elapsed since that path's FIRST sample and y is the measured byte count. DiskWatch fits the ordinary least-squares line y = intercept + slope * x: slope = sum((x - mean_x) * (y - mean_y)) / sum((x - mean_x)^2) intercept = mean_y - slope * mean_x The slope IS the growth rate in bytes per day. Goodness of fit is the standard coefficient of determination: R^2 = 1 - sum((y - fitted_y)^2) / sum((y - mean_y)^2) R^2 near 1.0 means the usage is tracking a straight line closely and the projection is worth acting on. A low R^2 means the growth is erratic and the projected date is a rough guess - collect more samples. The projected exhaustion point solves the fitted line for the capacity: x_full = (capacity - intercept) / slope days until = x_full - x_of_last_sample Note that the projection is anchored at the LAST SAMPLE, not at the moment you happen to run forecast, so re-running forecast without sampling does not change the answer. A NOTE ON SHORT SAMPLE WINDOWS Slope is always normalised to bytes PER DAY. If your samples are seconds apart, a few megabytes of growth extrapolates to an enormous per-day figure - tens of gigabytes per day is entirely normal for samples ten seconds apart. That is the arithmetic working correctly, not a bug. This is why every forecast prints the sample window duration next to the rate: a slope of 40 GiB/day measured over a 9-second window is a statement about 9 seconds, not a prediction about tomorrow. For projections you intend to trust, sample over hours or days. EDGE CASES, HANDLED EXPLICITLY fewer than 2 samples Reports "need at least 2 samples to fit a trend". Exits 0. No line is fitted and no number is invented. zero or negative slope Reports "not growing - no exhaustion projected". No projection, no division by a zero slope. already over capacity Reports how far over the tree already is and tells you to free space. Never prints a negative days-until-full. flat usage R^2 is undefined when the data has no variance (total sum of squares is zero). It is printed as "n/a" and emitted as null in JSON, not as a fake 1.0. identical timestamps If every sample shares one timestamp there is no time span to regress against; that is reported rather than dividing by zero. missing history file Tells you to run `sample` first and exits 1. WHAT IS MEASURED - AND WHAT IS NOT ================================== IMPLEMENTED, AND WORKING TODAY - Recursive byte usage of any directory tree you point it at. - Multiple independent watched paths inside a single history file. Each path is regressed separately, so a growing tree and a flat tree in the same history produce a projection for the growing one only. - Append-only JSON-lines history: durable, greppable, trivially parsed by anything, and never rewritten in place. - Least-squares linear regression with R-squared, on real timestamps. - Capacity exhaustion projection in days plus a calendar date. - Human-readable capacity strings and human-readable byte formatting. - --json output on forecast and history for monitoring integrations. - Unreadable files and directories are counted and skipped, never fatal, so a permission-denied subdirectory does not abort the sample. DELIBERATE LIMITS OF THE CURRENT MEASUREMENT - Sizes are APPARENT sizes (the file length as reported by the OS), summed over regular files. This is not the same as blocks actually consumed on disk. Sparse files therefore read LARGER than their real footprint, and filesystem compression, block-size rounding and deduplication are not accounted for. - Symbolic links are not followed and their targets are not counted, so a tree cannot be double-counted through a link loop. - Hard links are counted once per path encountered, so a file with two links inside the same tree is counted twice. - Directory entries themselves contribute no bytes; only file contents do. - Capacity is a number YOU state. DiskWatch does not discover it. - The trend model is strictly linear. Genuinely exponential growth will be under-projected, and a log-rotated tree that grows and drops in a sawtooth is not modelled well by a straight line - watch R-squared. NOT IMPLEMENTED - ROADMAP These need privileged access or native per-OS APIs that a portable, dependency free, standard-library-only binary cannot reach. They are honestly absent, not stubbed: - Real per-volume capacity and free space via statfs/statvfs (Unix) and GetDiskFreeSpaceEx (Windows), so --capacity could default to the actual size of the filesystem holding the watched path instead of being stated by hand. - True on-disk block usage (st_blocks) rather than apparent file size, including correct sparse-file and compressed-file accounting. - SMART attributes, drive temperature, reallocated-sector counts and remaining-life estimates. These require raw device ioctls and root or Administrator privileges. - Background scheduling. DiskWatch never daemonises and never installs itself. Run `sample` from cron, systemd timers, launchd or Task Scheduler; the append-only history is designed exactly for that. - Alerting: no email, webhook, Slack or exit-code threshold ("fail if fewer than N days remain"). Parse --json and act on it yourself for now. - Non-linear trend models (exponential, seasonal, sawtooth-aware) and confidence intervals on the projected date. - Network, cloud-bucket and remote filesystem targets. - Per-file or per-subdirectory attribution of the growth. DiskWatch tells you that a tree is growing and when it will fill; use DrivePulse to find out which files inside it are responsible. EXIT CODES ========== 0 Success. This includes forecasts that cannot project - "need at least 2 samples", "not growing" and "already over capacity" are legitimate answers, not failures. 1 Bad invocation (unknown command, missing required flag, unparseable capacity) or an I/O error (missing history file, unreadable watched path, corrupt history line). Explicit help exits 0. HISTORY FILE FORMAT =================== One JSON object per line, UTF-8, newline-terminated, appended in chronological order: {"ts":"2026-08-10T03:38:45.843510443Z","capacity":10485760, "entries":[{"path":"/tmp/data","bytes":2000000,"files":1,"dirs":0,"errors":0}]} ts RFC 3339 UTC timestamp, nanosecond precision capacity bytes, omitted when no --capacity was given entries one object per watched path path absolute path as resolved at sample time bytes total apparent size of regular files in the tree files number of regular files counted dirs number of subdirectories below the root errors entries skipped because they could not be read The format is deliberately boring. You can append to it from your own scripts, split it, archive it, or read it with one line of jq.