Why
Debian/Ubuntu hosts accumulate pending packages quietly until someone notices a security advisory or a surprise reboot into a large upgrade. FTMON's built-ins do not watch apt. This recipe counts upgradable packages (total and security pocket), ages the apt cache, and raises confirmed incidents when security work is waiting, the backlog crosses an operator threshold, or metadata goes stale. Trends on updates_total show whether the backlog is growing rather than being cleared.
Install
This is an original FTMON-maintained ftmon-json check (MIT). Install the script from the recipe (do not put it under FTMON's own data/state dirs):
# Dedicated / multi-user host:
sudo install -d -o root -g root -m 0755 /usr/local/lib/ftmon/checks
sudo install -o root -g root -m 0755 \
extra-monitors/package-updates/scripts/check_apt_updates \
/usr/local/lib/ftmon/checks/check_apt_updates
# Single-user desktop (daemon uid owns the file):
install -d -m 0755 ~/.local/lib/ftmon/checks
install -m 0755 \
extra-monitors/package-updates/scripts/check_apt_updates \
~/.local/lib/ftmon/checks/check_apt_updates
# then set argv[0] in checks.toml to that absolute path
Verify trust before registration:
ftmon check trust /usr/local/lib/ftmon/checks/check_apt_updates
Requires the host apt CLI. The check only runs apt list --upgradable and reads world-readable cache stamps under /var/lib/apt and /var/cache/apt. It never runs apt-get update or installs packages.
Configure
ftmon recipe install package-updates
Defaults (-w 20, cache stale at 7 days inside the check):
- **Security pending** (
updates_security > 0) is critical after two confirms — - **Many pending** warns when total upgradable packages exceed
- **Cache stale** warns when
cache_age_sexceeds seven days — usually means - **Updates rising** is a notice on sustained backlog growth (slope + monotonic
the operational signal for hosts that are not auto-upgraded.
updates_warn_count (keep checks.toml -w equal to that parameter so plugin_state mirrors the monitor rule).
unattended-upgrades / apt timers stopped refreshing indexes.
fraction over 6h), not a page by itself.
apt list --upgradable lines look like name/suite version arch [upgradable from: …] (four fields before the bracket). The check's regex must match that shape; an extra field matches nothing and silently reports zero packages.
Test
/usr/local/lib/ftmon/checks/check_apt_updates -w 20
echo "$?" # must be 0; severity is JSON state
Exit status is always **0** for ftmon-json. Severity is the JSON state field (0 OK, 1 warning, 2 critical, 3 unknown). Fixtures under fixtures/ match the live metric labels. Direct behavioral tests (offline):
uv run pytest -q extra-monitors/package-updates/tests
ftmon check
ftmon doctor
Security and permissions
No elevation and no network from the check itself: it invokes local apt and reads cache metadata. Keep the installed file root- or daemon-owned, mode 0755, not group/world-writable, and not a symlink (ftmon check trust enforces this). Remediation (apt update / apt full-upgrade) stays outside FTMON actions unless the operator adds a separate, reviewed action script.
Upstream and licence
Original FTMON recipe script under extra-monitors/package-updates, MIT. FTMON redistributes this script as the recipe's maintained check.
Verified on 2026-07-30 on Ubuntu 24.04.4 LTS: live check_apt_updates -w 20 returned exit 0 with updates_total=22, updates_security=0, cache_age in seconds, state=1 (above warn count); trust check passed for the installed path.
Registry example
[check.package_updates]
# -w must stay aligned with monitor.toml updates_warn_count (plugin_state is a
# convenience mirror; monitor rules are authoritative for alerting). Cache
# staleness defaults to 7 days inside the check (--cache-stale-s).
argv = [
"/usr/local/lib/ftmon/checks/check_apt_updates",
"-w", "20",
]
protocol = "ftmon-json"
# Outer FTMON timeout sits above apt list's 8s internal bound.
timeout = "12s"
Monitor definition
schema = 1
[monitor]
name = "package_updates"
description = "Pending apt package updates: security, total, and cache freshness (check_apt_updates)"
version = 1
enabled = false
platforms = ["linux"]
interval = "5m"
source = "external"
[source_options]
check = "package_updates"
entity = "apt"
[[source_options.perfdata]]
label = "updates_total"
metric = "updates_total"
plugin_uom = "packages"
unit = "packages"
kind = "gauge"
[[source_options.perfdata]]
label = "updates_security"
metric = "updates_security"
plugin_uom = "packages"
unit = "packages"
kind = "gauge"
[[source_options.perfdata]]
label = "cache_age"
metric = "cache_age_s"
plugin_uom = "s"
unit = "seconds"
kind = "gauge"
[parameters]
updates_warn_count = { value = 20, doc = "Pending non-security updates that warrant a warning — keep checks.toml -w in sync" }
cache_stale_warn_s = { value = 604800, doc = "Apt cache age (seconds) before warning — default 7 days" }
updates_growth_per_day = { value = 5, doc = "Pending-updates growth rate (per day) before notice" }
updates_growth_confidence_min = { value = 0.8, doc = "Required fraction of rising pending-updates samples" }
[glance]
metric = "updates_total"
unit = "packages"
aggregate = "max"
thresholds = [
{ label = "warn", parameter = "updates_warn_count" },
]
[[derived]]
name = "updates_total_rate_pd"
expr = 'slope(updates_total, "6h") * 86400'
[[derived]]
name = "updates_total_growth_confidence"
expr = 'monot(updates_total, "6h")'
[[derived]]
name = "cache_age_days"
expr = 'cache_age_s / 86400'
[[rule]]
id = "security-pending"
group = "apt-security"
when = "updates_security > 0"
severity = "critical"
confirm_cycles = 2
message = "{updates_security} security update(s) pending; run apt full-upgrade (or unattended-upgrade)"
[[rule]]
id = "cache-stale"
group = "apt-cache"
when = "cache_age_s > cache_stale_warn_s"
severity = "warning"
confirm_cycles = 2
message = "Apt cache is {cache_age_days:.0f} day(s) old; run apt update to refresh"
[[rule]]
id = "many-pending"
group = "apt-pending"
when = "updates_total > updates_warn_count"
severity = "warning"
confirm_cycles = 3
message = "{updates_total} package updates pending (warn at {updates_warn_count})"
[[rule]]
id = "updates-rising"
group = "apt-pending"
when = 'updates_total_rate_pd > updates_growth_per_day and updates_total_growth_confidence >= updates_growth_confidence_min'
severity = "notice"
confirm_cycles = 3
message = "Pending updates are growing steadily ({updates_total_rate_pd:.1f}/day)"
[[rule]]
id = "plugin-unknown"
group = "check-health"
when = "plugin_state == 3"
severity = "warning"
confirm_cycles = 2
message = "Package-updates check is unknown: {plugin_message}"
[[trend]]
id = "updates-growth"
kind = "growth"
title = "Pending package updates trend"
value_metric = "updates_total"
value_unit = "packages"
rate_metric = "updates_total_rate_pd"
rate_unit = "packages/day"
confidence_metric = "updates_total_growth_confidence"
confidence_threshold_param = "updates_growth_confidence_min"
rate_threshold_params = ["updates_growth_per_day"]