Practical infra coding

Find full disks in a metrics CSV with bad rows

easyCSV, JSON and YAML

Problem statement

A monitoring export lists disk usage per host and mount point in GB. Print every mount that is at least 85% full, fullest first, as host mount pct% with one decimal. Exit 1 if anything is over the threshold, so the script can drive a cron alert.

Some rows are bad, as they always are:

  • n/a where the agent did not report.
  • A size of 0 from a mount that was being detached.
  • A row cut short.

Skip each bad row and report it on stderr with its line number. The threshold is an optional second argument.

disks.csv

APACHE
host,mount,used_gb,size_gb
web-01,/,41.2,50
web-01,/var/log,9.1,10
db-01,/,20,50
db-01,/data,930,1000
cache-01,/,n/a,50
batch-01,/,12,0
batch-01,/scratch,45,50
web-02,/

Examples

Example 1

Input: python solution.py disks.csv

Output: db-01 /data 93.0% web-01 /var/log 91.0% batch-01 /scratch 90.0%

Explanation: stderr shows line 6: not a number, line 7: impossible values used=12.0 size=0.0 and line 9: expected 4 fields, got 2. web-01 / is 82.4%, below 85.

Hints

Approach

Optimal

Validate each row in the order that the checks can fail.

  1. Check the header exactly. If the export format changes, fail loudly instead of reading the wrong column.
  2. Skip empty rows. Reject rows without exactly 4 fields.
  3. Convert with float and reject anything that is not a number. Then reject values that are not finite, zero or negative sizes, and negative usage.
  4. Compute the percentage and keep rows at or above the threshold. Sort by (-pct, host, mount) so ties are deterministic.

Only alerting rows are kept (a of them). Problems are collected and printed after the results, so stdout stays clean for another program to read.

ComplexityTime O(n + a log a)Space O(a)
Python
import csv
import math
import sys
def full_disks(f, threshold=85.0):
reader = csv.reader(f)
header = next(reader, None)
if header != ["host", "mount", "used_gb", "size_gb"]:
raise ValueError(f"unexpected header: {header}")
alerts, problems = [], []
for row in reader:
line = reader.line_num
if not row:
continue
if len(row) != 4:
problems.append(f"line {line}: expected 4 fields, got {len(row)}")
continue
host, mount, used, size = row
try:
used, size = float(used), float(size)
except ValueError:
problems.append(f"line {line}: not a number")
continue
if not (math.isfinite(used) and math.isfinite(size)) or size <= 0 or used < 0:
problems.append(f"line {line}: impossible values used={used} size={size}")
continue
pct = used / size * 100
if pct >= threshold:
alerts.append((pct, host, mount))
alerts.sort(key=lambda a: (-a[0], a[1], a[2]))
return alerts, problems
def main(path, threshold=85.0):
with open(path, newline="", encoding="utf-8") as f:
try:
alerts, problems = full_disks(f, threshold)
except ValueError as e:
sys.exit(f"{path}: {e}")
for pct, host, mount in alerts:
print(f"{host} {mount} {pct:.1f}%")
for p in problems:
print(p, file=sys.stderr)
return 1 if alerts else 0
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "disks.csv"
limit = float(sys.argv[2]) if len(sys.argv) > 2 else 85.0
sys.exit(main(path, limit))

Follow-up questions

  • Also warn about mounts that grew more than 10 percentage points since yesterday's CSV.
  • Some rows give sizes as 512M or 1.5T. Parse units.
  • Send one alert per host listing all its full mounts, instead of one line per mount.

Frequently asked questions

It passes validation and shows as over 100%. That can be real: some filesystems report reserved blocks oddly, or the two numbers were sampled at different moments. Whether to flag it as bad data or as an alert is a judgement call. Say which you chose; reporting it is safer than hiding it.

Disk-full is one of the most common causes of outages, and exports from monitoring tools are messy. The interviewer wants to see that one bad row neither crashes the script nor gets silently counted as 0%, and that errors say which line to look at.