aboutsummaryrefslogtreecommitdiff
path: root/prometheus_pacman_exporter/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'prometheus_pacman_exporter/__init__.py')
-rw-r--r--prometheus_pacman_exporter/__init__.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/prometheus_pacman_exporter/__init__.py b/prometheus_pacman_exporter/__init__.py
new file mode 100644
index 0000000..baf69dd
--- /dev/null
+++ b/prometheus_pacman_exporter/__init__.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+
+# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import os
+import shlex
+import subprocess
+from pathlib import Path
+from typing import cast
+
+import click_extra as click
+from cloup.constraints import If, require_all
+from prometheus_client import write_to_textfile
+
+from . import metrics
+from .cli import FlexibleColorOption, debug, error, msg
+
+VERSION = "1.0.0"
+DESCRIPTION = "Collect pacman package metrics and export them for Prometheus."
+DATE_OF_CREATION = "2021-05-15"
+DATE_OF_REVISION = "2026-05-08"
+AUTHOR = "Dennis Fink <me+coding@dennisfink.me>"
+LICENSE = "BSD-3-Clause"
+
+
+def print_version(
+ ctx: click.Context, param: click.Parameter | None, value: bool
+) -> None:
+ """Print script metadata (name, version, description, author, dates) and exit.
+
+ :param ctx: Click context injected by :func:`click.pass_context`.
+ """
+ if not value or ctx.resilient_parsing:
+ return
+
+ click.echo(click.style("Scriptname:", fg="red", bold=True) + f" {ctx.info_name}")
+ click.echo(click.style("Version:", fg="green", bold=True) + f" {VERSION}")
+ click.echo(click.style("Description:", fg="yellow", bold=True) + f" {DESCRIPTION}")
+ click.echo(click.style("Author:", fg="blue", bold=True) + f" {AUTHOR}")
+ click.echo(
+ click.style("Date of creation:", fg="magenta", bold=True)
+ + f" {DATE_OF_CREATION}"
+ )
+ click.echo(
+ click.style("Date of revision:", fg="cyan", bold=True) + f" {DATE_OF_REVISION}"
+ )
+ click.echo(click.style("License:", fg="red", bold=True) + f" {LICENSE}")
+
+ click.echo("""Copyright (c) 2026 Dennis Fink <me+coding@dennisfink.me>.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.""")
+
+ ctx.exit()
+
+
+@click.pass_context
+def run_command(
+ ctx: click.Context, command: list[str]
+) -> subprocess.CompletedProcess[str]:
+ """Run a shell command and return the result.
+
+ Logs the command at debug level before execution. Exits the application
+ with the command's return code if it fails.
+
+ :param ctx: Click context injected by :func:`click.pass_context`.
+ :param command: Command and its arguments.
+ :return: Completed process with stdout and stderr captured.
+ """
+ debug("Running command", " ".join(command), err=True)
+ try:
+ return subprocess.run(
+ command, capture_output=True, encoding="utf-8", check=True
+ )
+ except subprocess.CalledProcessError as e:
+ error(f"Command ({' '.join(command)}) failed with exit code {e.returncode}")
+ if e.stderr:
+ error("stderr:", e.stderr.strip())
+ ctx.exit(e.returncode)
+
+
+@click.command(
+ context_settings={"help_option_names": ("-h", "--help", "-?")}, params=[]
+)
+@click.option(
+ "--textfile-collector-dir",
+ type=click.Path(
+ exists=True,
+ readable=True,
+ writable=True,
+ file_okay=False,
+ allow_dash=False,
+ path_type=Path,
+ ),
+ default="/var/lib/prometheus/node-exporter",
+ help="Directory where the Prometheus collector textfile will be written.",
+)
+@click.option(
+ "--use-checkupdates", "use_checkupdates_flag", is_flag=True, default=False
+)
+@click.option(
+ "--aur", "aur_flag", is_flag=True, default=False, help="Include AUR updates."
+)
+@click.option("--aur-command", help="Command used to check for AUR updates.")
+@click.constraint(
+ If("aur_flag", then=require_all).hidden(), ["aur_flag", "aur_command"]
+)
+@click.option("--color", cls=FlexibleColorOption)
+@click.no_color_option()
+@click.option(
+ "-q",
+ "--quiet",
+ "quiet_flag",
+ is_flag=True,
+ default=False,
+ help="Suppress all non-error output.",
+)
+@click.option(
+ "-v",
+ "--verbose",
+ "verbose_flag",
+ is_flag=True,
+ default=False,
+ help="Enable verbose output.",
+)
+@click.option(
+ "--version",
+ callback=print_version,
+ default=False,
+ expose_value=False,
+ help="Show version information and exit.",
+ is_eager=True,
+ is_flag=True,
+)
+@click.pass_context
+def prometheus_pacman_exporter(
+ ctx: click.Context,
+ textfile_collector_dir: Path,
+ use_checkupdates_flag: bool,
+ aur_flag: bool,
+ aur_command: str | None,
+ quiet_flag: bool,
+ verbose_flag: bool,
+):
+ """Collect pacman package metrics and export them for Prometheus.
+
+ Queries pacman for installed package statistics and available updates, then
+ writes them to a ``.prom`` file for consumption by the Prometheus node
+ exporter textfile collector.
+
+ By default, updateable packages are determined using ``pacman --query
+ --upgrades``. Use ``--use-checkupdates`` to query updates with
+ ``checkupdates`` instead. AUR update metrics can optionally be collected by
+ providing ``--aur`` together with ``--aur-command``.
+ """
+
+ ctx.ensure_object(dict)
+ ctx.obj["debug"] = os.environ.get("DEBUG", "").lower() in ("1", "true", "yes")
+ ctx.obj["quiet"] = quiet_flag
+ ctx.obj["verbose"] = verbose_flag
+
+ debug("Effective configuration:", err=True)
+ debug(" textfile_collector_dir:", str(textfile_collector_dir), err=True)
+ debug(" use_checkupdates:", str(use_checkupdates_flag), err=True)
+ debug(" aur:", str(aur_flag), err=True)
+ debug(" aur_command:", str(aur_command), err=True)
+ debug(" verbose:", str(verbose_flag), err=True)
+ debug(" quiet:", str(quiet_flag), err=True)
+
+ pacman = ["pacman", "--query"]
+ queries = [
+ ([*pacman], metrics.INSTALLED_PACKAGES),
+ ([*pacman, "--explicit"], metrics.EXPLICIT_PACKAGES),
+ ([*pacman, "--deps"], metrics.DEPENDS_PACKAGES),
+ ([*pacman, "--unrequired"], metrics.UNREQUIRED_PACKAGES),
+ ([*pacman, "--foreign"], metrics.FOREIGN_PACKAGES),
+ ([*pacman, "--native"], metrics.NATIVE_PACKAGES),
+ ([*pacman, "--deps", "--unrequired"], metrics.ORPHAN_PACKAGES),
+ (
+ ["checkupdates"] if use_checkupdates_flag else [*pacman, "--upgrades"],
+ metrics.UPDATEABLE_PACKAGES,
+ ),
+ ]
+
+ if aur_flag:
+ queries.append(
+ (shlex.split(cast(str, aur_command)), metrics.AUR_UPDATEABLE_PACKAGES)
+ )
+
+ for command, metric in queries:
+ output = run_command(command)
+ metric.set(len(output.stdout.splitlines()))
+
+ metrics_file = textfile_collector_dir / "pacman.prom"
+ write_to_textfile(str(metrics_file.absolute()), metrics.REGISTRY)
+ msg("Metrics written to:", str(metrics_file.absolute()), err=True)
+
+
+if __name__ == "__main__":
+ prometheus_pacman_exporter()