Scrippy scripts are meant to run in production, where long stack traces scare users and leak information. In this step, you will see how Scrippy handles the errors your script does not handle, and how disk_watch.py handles the errors it can anticipate.
With the code of the previous step, check a mount point that does not exist:
./disk_watch.py --mount-point / /nope
[...]
[20261008 19:13:54] [INFO ] '-> /: 34.9% [OK]
[20261008 19:13:54] [INFO ] [+] Workspace deletion: /home/luiggi/.local/share/scrippy/tmp/disk_watch_1791447234.2610497_29610
[20261008 19:13:54] [INFO ] [+] End: 1 (FileNotFoundError)
[20261008 19:13:54] [CRITICAL] [FileNotFoundError]: [Errno 2] No such file or directory: '/nope'
The FileNotFoundError exception raised by shutil.disk_usage() is not handled by the script. It propagates up to the execution context, which:
End: 1 (FileNotFoundError)),CRITICAL message made of the exception name and message, without stack trace,1.In DEBUG level, the stack trace is displayed after the message, on the error output:
./disk_watch.py --mount-point / /nope --debug
[...]
[20261008 19:13:54] [CRITICAL] [FileNotFoundError]: [Errno 2] No such file or directory: '/nope'
File "/home/luiggi/tutorial/disk_watch.py", line 130, in main
results = check(get_mount_points(config, args), warning, critical)
File "/home/luiggi/tutorial/disk_watch.py", line 89, in check
usage = get_usage(mount_point)
File "/home/luiggi/tutorial/disk_watch.py", line 82, in get_usage
usage = shutil.disk_usage(mount_point)
File "/usr/lib/python3.12/shutil.py", line 1350, in disk_usage
st = os.statvfs(path)
Tip: Debug your scripts in a test environment, with the --debug option.
This default behavior guarantees a clean exit, but disk_watch.py stopped at the first faulty mount point and returned 1, which means WARNING for a monitoring tool. It can do better.
#!/usr/bin/env python3
"""
--------------------------------------------------------------------------------
@author : Luiggi Vercotti
@date : 2026-10-08
@version : 0.8.0
@description : Monitor filesystem usage
--------------------------------------------------------------------------------
Update:
0.1.0 2026-10-08 - Luiggi Vercotti - cre: Script creation
0.2.0 2026-10-08 - Luiggi Vercotti - evo: Several mount points, log levels
0.3.0 2026-10-08 - Luiggi Vercotti - evo: Monitoring exit status
0.4.0 2026-10-08 - Luiggi Vercotti - evo: Configuration file
0.5.0 2026-10-08 - Luiggi Vercotti - evo: Command line options
0.6.0 2026-10-08 - Luiggi Vercotti - evo: Alerting API token
0.7.0 2026-10-08 - Luiggi Vercotti - evo: Report file
0.8.0 2026-10-08 - Luiggi Vercotti - evo: Error handling
--------------------------------------------------------------------------------
List of mandatory configuration parameters:
@conf:filesystems|mount_points|str|false
@conf:thresholds|warning|int|false
@conf:thresholds|critical|int|false
@conf:alerting|token|str|true
--------------------------------------------------------------------------------
List of execution options and argument parameters:
@args:mount-point|str|Mount points to check (default: from configuration)|+||exclude||false|false
@args:exclude|str|Mount points to skip|+||||false|false
@args:warning|int|Warning threshold in percent (default: from configuration)|1||||false|false
@args:critical|int|Critical threshold in percent (default: from configuration)|1||||false|false
@args:token|str|Alerting API token (default: from configuration)|1||||false|true
@args:report|bool|Write a report into the report directory||false|||false|false
@args:format|choice|Report format (default: text)|1|text,csv||report|false|false
--------------------------------------------------------------------------------
Functioning:
---------------
Checks the usage of each mount point against a warning and a critical
threshold and exits with a monitoring compatible exit status:
0: OK, 1: WARNING, 2: CRITICAL, 3: UNKNOWN (a mount point can not be checked)
Configuration:
- filesystems::mount_points: Comma separated list of mount points
- thresholds::warning: Warning threshold in percent
- thresholds::critical: Critical threshold in percent
- alerting::token: Alerting API token (secret)
"""
# ------------------------------------------------------------------------------
# Initialization of the environment
# ------------------------------------------------------------------------------
import os
import sys
import shutil
import datetime
import scrippy_core
from scrippy_core import logger, ScrippyCoreError
OK, WARNING, CRITICAL, UNKNOWN = 0, 1, 2, 3
STATUS = {OK: "OK", WARNING: "WARNING", CRITICAL: "CRITICAL", UNKNOWN: "UNKNOWN"}
EXTENSIONS = {"text": "txt", "csv": "csv"}
# ------------------------------------------------------------------------------
# Definition of functions and classes
# ------------------------------------------------------------------------------
def get_mount_points(config, args):
if args.mount_point:
return args.mount_point
mount_points = [mp.strip() for mp in config.get("filesystems", "mount_points").split(",")]
return [mp for mp in mount_points if mp not in (args.exclude or [])]
def get_thresholds(config, args):
warning = args.warning if args.warning is not None else config.get("thresholds", "warning", "int")
critical = args.critical if args.critical is not None else config.get("thresholds", "critical", "int")
if not 0 < warning < critical <= 100:
raise ScrippyCoreError(f"Invalid thresholds: warning={warning}, critical={critical}")
return warning, critical
def get_usage(mount_point):
logger.debug(f"[+] Checking {mount_point}")
usage = shutil.disk_usage(mount_point)
return round(usage.used * 100 / usage.total, 1)
def check(mount_points, warning, critical):
results = []
for mount_point in mount_points:
try:
usage = get_usage(mount_point)
except OSError as err:
logger.error(f" '-> {mount_point}: {err.strerror}")
results.append((mount_point, None, UNKNOWN))
continue
if usage >= critical:
logger.error(f" '-> {mount_point}: {usage}% [CRITICAL]")
status = CRITICAL
elif usage >= warning:
logger.warning(f" '-> {mount_point}: {usage}% [WARNING]")
status = WARNING
else:
logger.info(f" '-> {mount_point}: {usage}% [OK]")
status = OK
results.append((mount_point, usage, status))
return results
def write_report(results, report_format, workspace_path, reportdir):
name = f"disk_watch_{datetime.datetime.now():%Y%m%d_%H%M%S}.{EXTENSIONS[report_format]}"
tmp_report = os.path.join(workspace_path, name)
logger.info(f"[+] Writing report: {tmp_report}")
with open(tmp_report, mode="w", encoding="utf-8") as report:
for mount_point, usage, status in results:
if report_format == "csv":
report.write(f"{mount_point};{usage};{STATUS[status]}\n")
else:
report.write(f"{mount_point:<20} {usage}% {STATUS[status]}\n")
report_dir = os.path.join(reportdir, "disk_watch")
os.makedirs(report_dir, exist_ok=True)
logger.info(f"[+] Publishing report into {report_dir}")
shutil.move(tmp_report, os.path.join(report_dir, name))
# ------------------------------------------------------------------------------
# Main processing
# ------------------------------------------------------------------------------
def main():
with scrippy_core.ScriptContext(retention=100) as _context:
config = _context.config
args = _context.args
token = args.token or config.get("alerting", "token")
logger.debug(f"[+] Alerting API token: {token}")
warning, critical = get_thresholds(config, args)
logger.log_section("disk_watch", f"Thresholds: warning={warning}% critical={critical}%")
results = check(get_mount_points(config, args), warning, critical)
if args.report:
write_report(results, args.format or "text", _context.workspace_path, _context.env["reportdir"])
status = max(status for _, _, status in results)
logger.info(f"[+] Status: {STATUS[status]}")
if status != OK:
sys.exit(status)
# ------------------------------------------------------------------------------
# Entry point
# ------------------------------------------------------------------------------
if __name__ == '__main__':
main()
What changed:
check() catches the OSError raised by get_usage(): The faulty mount point is logged as an error and gets the new UNKNOWN (3) status, then the next mount points are checked.get_thresholds() checks the consistency of the thresholds and raises a ScrippyCoreError if they are invalid../disk_watch.py --mount-point / /nope
[...]
[20261008 19:14:03] [INFO ] '-> /: 34.9% [OK]
[20261008 19:14:03] [ERROR ] '-> /nope: No such file or directory
[20261008 19:14:03] [INFO ] [+] Status: UNKNOWN
[20261008 19:14:03] [INFO ] [+] Workspace deletion: /home/luiggi/.local/share/scrippy/tmp/disk_watch_1791447243.9663746_29643
[20261008 19:14:03] [INFO ] [+] End: 3 (SystemExit)
[20261008 19:14:03] [CRITICAL] [SystemExit]: 3
./disk_watch.py --warning 95
[...]
[20261008 19:14:04] [INFO ] [+] End: 1 (ScrippyCoreError)
[20261008 19:14:04] [CRITICAL] [ScrippyCoreError]: Invalid thresholds: warning=95, critical=90
Catch an exception where you know what to do with it: check() knows that a faulty mount point must not prevent the others from being checked, so it handles the error itself.
Catch specific exceptions (OSError), not all exceptions: An unexpected error, i.e. a bug, must still stop the script.
When the script can not go on, there is no need to catch the error, log it and call sys.exit(1): Let the exception propagate, the execution context does exactly that.
ScrippyCoreErrorTo stop the script because of a condition detected by your code, raise a scrippy_core.ScrippyCoreError with a clear message:
from scrippy_core import ScrippyCoreError
raise ScrippyCoreError(f"Invalid thresholds: warning={warning}, critical={critical}")
Scrippy itself raises ScrippyCoreError for all its errors: Invalid header, invalid configuration, unauthorized user...
Each Scrippy module has its own exception class (i.e: ScrippyRemoteError for scrippy-remote), handled the same way.
| Situation | Exit status | Recorded in history |
|---|---|---|
Normal end, sys.exit() or sys.exit(0) |
0 |
Yes |
sys.exit(n) |
n |
Yes |
Unhandled exception, including ScrippyCoreError and KeyboardInterrupt (Ctrl+C) |
1 |
Yes |
| Invalid configuration file, unauthorized user, concurrent execution refused | 1 |
No |
| Invalid header declaration | 1 |
No |
| Invalid command line options | 2 |
No |
Note: Errors in the main configuration file (missing key, invalid YAML) occur while importing scrippy_core, before Scrippy is able to handle errors: They are displayed with a Python stack trace.
disk_watch.py is robust. In the next step, you will restrict who is allowed to run it.