aboutsummaryrefslogtreecommitdiff
path: root/prometheus_borgmatic_exporter/__init__.py
diff options
context:
space:
mode:
authorDennis Fink2026-09-19 17:27:56 +0200
committerDennis Fink2026-09-19 17:27:56 +0200
commit3a52d4cf01e4443aba1b9f78152f673330274295 (patch)
treec0655285a2ebebd59a1aacbea9a5122a5ffe0b42 /prometheus_borgmatic_exporter/__init__.py
parent1eab7199f985a9f659105291ade7706e518f275d (diff)
downloadprometheus-borgmatic-exporter-3a52d4cf01e4443aba1b9f78152f673330274295.tar.gz
prometheus-borgmatic-exporter-3a52d4cf01e4443aba1b9f78152f673330274295.zip
fix(cli): route diagnostic output to stderr
Keep diagnostic and progress messages separate from normal stdout output so the exporter can be used safely in pipelines and command substitutions. Allow message helpers to operate without an active Click context, while preserving quiet, verbose, and debug behavior when a context is available. Require Python 3.12 for the generic type parameter syntax used by the new context-object decorator and refresh the lockfile accordingly.
Diffstat (limited to 'prometheus_borgmatic_exporter/__init__.py')
-rw-r--r--prometheus_borgmatic_exporter/__init__.py134
1 files changed, 59 insertions, 75 deletions
diff --git a/prometheus_borgmatic_exporter/__init__.py b/prometheus_borgmatic_exporter/__init__.py
index 2cfcce5..8bfd629 100644
--- a/prometheus_borgmatic_exporter/__init__.py
+++ b/prometheus_borgmatic_exporter/__init__.py
@@ -89,14 +89,11 @@ def run_command(
Logs the command at debug level before execution. Exits the application
with the command's return code if it fails.
- Args:
- ctx: The Click context, injected by @click.pass_context.
- command: The command and its arguments as a list of strings.
-
- Returns:
- The completed process result with stdout and stderr captured.
+ :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))
+ debug("Running command", " ".join(command), err=True)
try:
return subprocess.run(
command, capture_output=True, encoding="utf-8", check=True
@@ -112,15 +109,12 @@ def run_command(
def load_json(ctx: click.Context, raw: str) -> Any:
"""Parse a JSON string and return the result.
- Exits the application with code 1 if the input is not valid JSON, printing
- the parse error via the error() helper.
-
- Args:
- ctx: The Click context, injected by @click.pass_context.
- raw: The raw JSON string to parse.
+ Exits the application with status code 1 if the input is not valid JSON and
+ reports the parsing error using :func:`error`.
- Returns:
- The parsed JSON value.
+ :param ctx: Click context injected by :func:`click.pass_context`.
+ :param raw: Raw JSON string to parse.
+ :return: Parsed JSON value.
"""
try:
return json.loads(raw)
@@ -132,15 +126,11 @@ def load_json(ctx: click.Context, raw: str) -> Any:
def extract_archive_counts(
repositories: list[types.BorgInfoReturnType] | list[types.BorgListReturnType],
) -> list[types.ArchiveCount]:
- """Extract archive counts and repository metadata from borgmatic info output.
+ """Extract archive counts and repository metadata from borgmatic output.
- Args:
- repositories: List of repository objects as returned by borgmatic info
- or borgmatic list.
-
- Returns:
- A list of ArchiveCount dicts, one per repository, containing the
- repository id, label, location, and total number of archives.
+ :param repositories: Repository objects returned by ``borgmatic info`` or
+ ``borgmatic list``.
+ :return: Archive counts and identifying metadata for each repository.
"""
return [
{
@@ -158,29 +148,24 @@ def fetch_repositories(
) -> tuple[list[types.BorgInfoReturnType], list[types.ArchiveCount]]:
"""Fetch repository and archive data from borgmatic.
- When all_archives is False, runs `borgmatic info --archive latest` and
- `borgmatic list` in complementary intent: info provides per-repository
- stats and the latest archive metrics, while list provides the total archive
- count — since info with --archive latest only returns a single archive,
- making len() unreliable for counting.
-
- When all_archives is True, only `borgmatic info` is run. The full archive
- list is available directly from the info output, so no separate list call
- is needed and archive counts are derived from it.
+ When ``all_archives`` is false, runs ``borgmatic info --archive latest``
+ and ``borgmatic list``. The former provides per-repository statistics and
+ metrics for the latest archive, while the latter provides the complete
+ archive list needed to determine archive counts.
- Args:
- borgmatic_bin: Path to the borgmatic executable.
- borgmatic_config: Path to the borgmatic configuration file.
- all_archives: If True, fetch and return metrics for all archives.
- This is significantly slower for large repositories.
+ When ``all_archives`` is true, only ``borgmatic info`` is run. Its output
+ already contains the complete archive list, so archive counts can be
+ derived directly from it.
- Returns:
- A tuple of:
- - repositories: Raw borgmatic info output, one entry per repository.
- - archive_counts: Archive count and identifying metadata per repository,
- always populated regardless of the all_archives flag.
+ :param borgmatic_bin: Path to the borgmatic executable.
+ :param borgmatic_configs: Paths to borgmatic configuration files or
+ directories.
+ :param all_archives: Whether to fetch metrics for all archives instead of
+ only the latest archive.
+ :return: Raw ``borgmatic info`` output and archive counts with identifying
+ metadata for each repository.
"""
- verbose("Fetching repositories")
+ verbose("Fetching repositories", err=True)
borgmatic_cmd = [str(borgmatic_bin)]
for config in borgmatic_configs:
@@ -193,7 +178,7 @@ def fetch_repositories(
repositories = cast(
list[types.BorgInfoReturnType], load_json(borgmatic_info.stdout)
)
- debug(f"Fetched {len(repositories)} repositories")
+ debug(f"Fetched {len(repositories)} repositories", err=True)
return repositories, extract_archive_counts(repositories)
info_command += ["--archive", "latest"]
@@ -201,7 +186,7 @@ def fetch_repositories(
repositories = cast(
list[types.BorgInfoReturnType], load_json(borgmatic_info.stdout)
)
- debug(f"Fetched {len(repositories)} repositories")
+ debug(f"Fetched {len(repositories)} repositories", err=True)
borgmatic_list = run_command([*borgmatic_cmd, "list", "--json"])
archives = cast(list[types.BorgListReturnType], load_json(borgmatic_list.stdout))
@@ -211,16 +196,14 @@ def fetch_repositories(
def collect_repository_metrics(
repository: types.BorgInfoReturnType, all_archives: bool
) -> None:
- """Collect and set all Prometheus metrics for a single repository.
+ """Collect Prometheus metrics for a repository.
- Sets cache stats, encryption info, last modified timestamp, and latest
- archive metrics. When all_archives is True, also collects metrics for every
- individual archive by calling _collect_archive_metrics.
+ Sets cache statistics, encryption information, the last-modified timestamp,
+ and metrics for the latest archive. When ``all_archives`` is true, also
+ collects metrics for every individual archive.
- Args:
- repository: Repository info as returned by borgmatic.
- all_archives: If True, collect metrics for all archives, not just
- the latest.
+ :param repository: Repository information returned by borgmatic.
+ :param all_archives: Whether to collect metrics for every archive.
"""
repository_metadata = repository["repository"]
@@ -229,6 +212,7 @@ def collect_repository_metrics(
repository_metadata["label"],
f"({repository_metadata['id']})",
repository_metadata["location"],
+ err=True,
)
repository_labels: types.RepositoryLabels = {
@@ -260,11 +244,15 @@ def collect_repository_metrics(
metric.labels(**repository_labels).set(repository_stats[key])
if not repository["archives"]:
- warn("No archives found for repository:", repository_metadata["label"])
+ warn(
+ "No archives found for repository:", repository_metadata["label"], err=True
+ )
return
latest_archive = max(repository["archives"], key=lambda archive: archive["start"])
- debug(f"Latest archive {latest_archive['name']}:", latest_archive["start"])
+ debug(
+ f"Latest archive {latest_archive['name']}:", latest_archive["start"], err=True
+ )
metrics.LATEST_ARCHIVE_DURATION.labels(**repository_labels).set(
latest_archive["duration"]
@@ -287,14 +275,12 @@ def collect_repository_metrics(
def collect_archive_metrics(
archive: types.BorgArchive, repository_labels: types.RepositoryLabels
) -> None:
- """Collect and set Prometheus metrics for a single archive.
+ """Collect Prometheus metrics for an archive.
- Args:
- archive: Archive info as returned by borgmatic.
- repository_labels: The parent repository's label dict, used to
- populate the shared label dimensions on each metric.
+ :param archive: Archive information returned by borgmatic.
+ :param repository_labels: Labels inherited from the parent repository.
"""
- msg("Processing archive:", archive["name"], f"({archive['id']})")
+ msg("Processing archive:", archive["name"], f"({archive['id']})", err=True)
archive_labels = {
**repository_labels,
@@ -406,15 +392,12 @@ def prometheus_borgmatic_exporter(
"""Collect borgmatic repository metrics and export them for Prometheus.
Queries borgmatic for repository and archive statistics and writes them to
- a .prom file for consumption by the Prometheus node exporter textfile
+ a ``.prom`` file for consumption by the Prometheus node exporter textfile
collector.
- By default only the latest archive is queried per repository. Use
- --all-archives to collect metrics for every archive, at the cost of a
- significantly longer runtime.
-
- Using --all-archives will create one time series per archive and may
- significantly increase Prometheus storage usage.
+ By default, only the latest archive is queried for each repository. Use
+ ``--all-archives`` to collect metrics for every archive, at the cost of a
+ significantly longer runtime and increased Prometheus storage usage.
"""
ctx.ensure_object(dict)
@@ -422,17 +405,18 @@ def prometheus_borgmatic_exporter(
ctx.obj["quiet"] = quiet_flag
ctx.obj["verbose"] = verbose_flag
- debug("Effective configuration:")
- debug(" borgmatic_bin:", str(borgmatic_bin))
+ debug("Effective configuration:", err=True)
+ debug(" borgmatic_bin:", str(borgmatic_bin), err=True)
debug(
" borgmatic_configs:",
", ".join(str(config) for config in borgmatic_configs)
or "<borgmatic defaults>",
+ err=True,
)
- debug(" textfile_collector_dir:", str(textfile_collector_dir))
- debug(" all_archives:", str(all_archives_flag))
- debug(" verbose:", str(verbose_flag))
- debug(" quiet:", str(quiet_flag))
+ debug(" textfile_collector_dir:", str(textfile_collector_dir), err=True)
+ debug(" all_archives:", str(all_archives_flag), err=True)
+ debug(" verbose:", str(verbose_flag), err=True)
+ debug(" quiet:", str(quiet_flag), err=True)
repositories, archive_counts = fetch_repositories(
borgmatic_bin, borgmatic_configs, all_archives_flag
@@ -450,7 +434,7 @@ def prometheus_borgmatic_exporter(
metrics_file = textfile_collector_dir / "borgmatic.prom"
write_to_textfile(str(metrics_file.absolute()), metrics.REGISTRY)
- msg("Metrics written to:", str(metrics_file.absolute()))
+ msg("Metrics written to:", str(metrics_file.absolute()), err=True)
if __name__ == "__main__":