A Prometheus exporter for Ubuntu package update status, based on the machine
readable API of the Ubuntu Pro client
(pro api u.pro.packages.updates.v1).
Most apt-based exporters count upgradable packages by parsing apt output or
guessing from repository names. This exporter asks the Ubuntu Pro client
instead. The client classifies every pending update into a pocket
(standard-security, standard-updates, esm-apps, esm-infra) and an
update status. One of those statuses is pending_attach: a security fix that
exists in Ubuntu's ESM repositories but cannot be installed because the host
is not attached to an Ubuntu Pro subscription. That makes it possible to
alert on the security updates a host is missing, not just the ones it can
install.
No subscription is required. The API endpoints used here work on unattached
hosts, need no root privileges and no network access. They read the local apt
caches, so results are as fresh as the last apt update.
- Ubuntu with
ubuntu-advantage-tools27.12 or newer. This ships by default on all supported releases and providespro api u.pro.packages.updates.v1andu.pro.security.status.reboot_required.v1. - Periodic apt metadata refresh (
APT::Periodic::Update-Package-Lists, enabled by default on Ubuntu). The exporter never runsapt updateitself.
If the pro client is missing or fails, the exporter keeps serving with
ubuntu_pro_updates_exporter_up set to 0. It never crashes on a degraded host.
| Metric | Type | Labels | Meaning |
|---|---|---|---|
ubuntu_pro_updates_exporter_up |
gauge | 1 if the last refresh from the pro client succeeded | |
ubuntu_pro_updates_pending |
gauge | pocket, status |
Number of pending package updates |
ubuntu_pro_updates_download_bytes |
gauge | pocket |
Total download size of pending updates |
ubuntu_pro_updates_reboot_required |
gauge | state |
Reboot required state, encoded as an enum where the active state is 1 |
ubuntu_pro_updates_exporter_last_success_timestamp_seconds |
gauge | Unix time of the last successful refresh, absent until one succeeds | |
ubuntu_pro_updates_exporter_query_duration_seconds |
gauge | Time spent querying the pro client during the last refresh | |
ubuntu_pro_updates_exporter_build_info |
gauge | version, revision, goversion |
Build information |
Label values are fixed and low cardinality. All series are always exported, at 0 when empty, so alerts never have to deal with absent series.
pocket:standard-security,standard-updates,esm-apps,esm-infrastatus:upgrade_available,upgrade_available_not_preferred,pending_attach(the fix exists in ESM but the host is unattached),pending_enable(attached, but the ESM service is disabled) andupgrade_unavailable(attached, but not entitled)state:no,yesandyes-kernel-livepatches-applied(a reboot is pending but Livepatch covers the running kernel)
There is deliberately no total gauge. The sum of ubuntu_pro_updates_pending
equals the num_updates field of the API, and a gauge named *_total would
collide with counter naming conventions.
# Pending security updates (standard pocket) per host
sum by (instance) (ubuntu_pro_updates_pending{pocket="standard-security"})
# Security fixes a host is missing because it is not attached to Ubuntu Pro
sum by (instance) (ubuntu_pro_updates_pending{status="pending_attach"})
# Hosts needing a reboot, excluding those covered by Livepatch
ubuntu_pro_updates_reboot_required{state="yes"} == 1
# Exporter healthy but data stale for a day
time() - ubuntu_pro_updates_exporter_last_success_timestamp_seconds > 86400
Per-package labeled metrics are intentionally not exposed. A host that has
not been upgraded in a while can easily have several hundred pending updates,
and labels like package and version would turn those into hundreds of
churning time series per host. Prometheus is not the right store for that
kind of data.
Instead, run with --log.package-updates (ideally combined with
--log.format=json). Whenever the set of pending updates changes, the
exporter logs the full list with package, version, pocket and status as one
structured log entry. Ship that to your log store and join on hostname and
time. The metrics tell you that updates are pending and how many, the log
tells you which.
Download the static binary for your architecture (linux amd64 or arm64) from the releases page and put it on the host. That is the whole install. The project deliberately ships just the binary; run it under your process supervisor of choice.
The toolchain is managed with mise, see .mise.toml:
mise install # Go and goreleaser
make # gofmt check, go vet, tests, build
make snapshot # goreleaser build --snapshot --cleanReleases are built with goreleaser, see
.goreleaser.yaml. CI runs fmt, vet, build and tests on every push and pull
request. Pushing a v* tag drafts a GitHub release with the binaries
attached.
./ubuntu-pro-updates-exporter --web.listen-address=:10052 --web.telemetry-path=/metrics| Flag | Default | Description |
|---|---|---|
--web.listen-address |
:10052 |
Address to expose metrics on |
--web.telemetry-path |
/metrics |
Metrics path |
--pro.binary |
pro |
Ubuntu Pro client executable |
--pro.timeout |
30s |
Timeout per pro api invocation |
--pro.refresh-interval |
12h |
How often to refresh data from the pro client |
--log.format |
text |
text or json |
--log.package-updates |
false |
Log the pending update list when it changes |
--version |
Print version and exit |
Update data is refreshed by a background loop every --pro.refresh-interval.
Scrapes serve the cached result instantly, so no special scrape timeout is
needed:
scrape_configs:
- job_name: ubuntu_pro_updates
scrape_interval: 1m
static_configs:
- targets: ["myhost:10052"]The exporter needs no root privileges, so run it as an unprivileged user.
When running without a home directory (for example with systemd
DynamicUser=yes), point $HOME at a writable directory, because the pro
client writes a per-user log under $HOME/.cache when invoked unprivileged.
Port 10052 is the port registered for this exporter in the Prometheus default port allocations.
Why shell out to pro api? It is the only stable machine readable interface
of the pro client. The underlying API is an in-process Python library, and
there is no socket or D-Bus service. The CLI prints a versioned JSON envelope
to stdout even on failure, so the exporter parses JSON exclusively, never
exit codes or English text, and surfaces the error codes of the envelope
itself.
Collection is decoupled from serving. A single pro api walk of the apt
cache costs seconds of CPU, which would make every scrape slow and let
concurrent scrapes pile up pro processes. The background loop refreshes the
data instead, and scrapes serve the cached snapshot. The default interval of
12 hours mirrors the cadence of apt-daily, whose timer runs twice a day and
refreshes package lists at most once per day. When a refresh fails, the
detail metrics are dropped rather than served stale, and ubuntu_pro_updates_exporter_up
together with ubuntu_pro_updates_exporter_last_success_timestamp_seconds keeps failure and
staleness alertable.
The reboot required query is best effort. If it fails while the updates
query succeeds, ubuntu_pro_updates_exporter_up stays 1 and only the reboot metric is
omitted.
MIT (see LICENSE).