diff options
| author | Dennis Fink | 2026-09-06 23:24:20 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-09-06 23:24:20 +0200 |
| commit | 8fedec6a333d089e4f73838333773c74bd74e515 (patch) | |
| tree | 9402581ae393c2ff6d0bba45a9c9dcabb490c079 /prometheus_borgmatic_exporter | |
| parent | 66d0fadd5e364eb8fceb39e8061cd93a01e9f210 (diff) | |
| download | prometheus-borgmatic-exporter-8fedec6a333d089e4f73838333773c74bd74e515.tar.gz prometheus-borgmatic-exporter-8fedec6a333d089e4f73838333773c74bd74e515.zip | |
refactor(cli): extract helpers and improve option handling
Move shared CLI output helpers into a dedicated module to keep the main
exporter module focused on metric collection.
Also simplify the command-line option definitions, add missing help
text, and allow --color values to be passed with or without an equals
sign.
Diffstat (limited to '')
| -rw-r--r-- | prometheus_borgmatic_exporter/__init__.py | 206 | ||||
| -rw-r--r-- | prometheus_borgmatic_exporter/cli.py | 116 |
2 files changed, 161 insertions, 161 deletions
diff --git a/prometheus_borgmatic_exporter/__init__.py b/prometheus_borgmatic_exporter/__init__.py index fea8dbd..373d8ca 100644 --- a/prometheus_borgmatic_exporter/__init__.py +++ b/prometheus_borgmatic_exporter/__init__.py @@ -16,13 +16,7 @@ import click_extra as click from prometheus_client import write_to_textfile from . import metrics, types - -############################################################################### -# SCRIPT METADATA -# -# These variables describe the script and are used for the --version output -# and for informational messages. -############################################################################### +from .cli import FlexibleColorOption, debug, error, msg, verbose, warn VERSION = "1.0.0" DESCRIPTION = "Collect borgmatic repository metrics and export them for Prometheus." @@ -32,124 +26,8 @@ AUTHOR = "Dennis Fink <me+coding@dennisfink.me>" LICENSE = "BSD-3-Clause" -############################################################################### -# FUNCTIONS -# -# This section defines the helper functions used throughout the script. They -# handle formatted output, error reporting, version information, utilities -# functions and runtime behaviour such as displaying usage instructions. -############################################################################### - - -def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None: - """Render a styled prefix and message to stdout. - - Internal helper used by error(), msg(), warn(), verbose(), and debug() - to avoid duplicating the Click styling and echo logic. The first message - argument is rendered in bold, subsequent arguments are appended unstyled. - - Args: - prefix: The prefix string shown before the message (e.g. "==>", "==> ERROR:"). - color: A Click-compatible color name (e.g. "red", "green") applied to the prefix. - *message: One or more message parts to display. At least one is required. - enabled: If False, the function returns immediately without printing. - Defaults to True. - - Raises: - TypeError: If no message arguments are provided. - """ - if not message: - raise TypeError("emit() missing 1 required positional argument: 'message'") - - if not enabled: - return - - click.echo( - " ".join( - [ - click.style(prefix, fg=color, bold=True), - click.style(message[0], bold=True), - *message[1:], - ] - ) - ) - - -def error(*message: str) -> None: - """Print a formatted error message to stdout in red. - - The first argument is rendered in bold, subsequent arguments are appended - unstyled. Always prints regardless of quiet or verbose flags. - - Args: - *message: One or more message parts to display. - """ - emit("==> ERROR:", "red", *message) - - -@click.pass_obj -def msg(obj: dict[str, bool], *message: str) -> None: - """Print a formatted informational message to stdout in green. - - Suppressed when the quiet flag is set. The first argument is rendered - in bold, subsequent arguments are appended unstyled. - - Args: - *message: One or more message parts to display. - """ - emit("==>", "green", *message, enabled=not obj.get("quiet", False)) - - -@click.pass_obj -def warn(obj: dict[str, bool], *message: str) -> None: - """Print a formatted warning message to stdout in yellow. - - Suppressed when the quiet flag is set. The first argument is rendered - in bold, subsequent arguments are appended unstyled. - - Args: - *message: One or more message parts to display. - """ - emit("==>", "yellow", *message, enabled=not obj.get("quiet", False)) - - -@click.pass_obj -def verbose(obj: dict[str, bool], *message: str) -> None: - """Print a formatted verbose message to stdout in blue. - - Only prints when the verbose flag is set and the quiet flag is not. - The first argument is rendered in bold, subsequent arguments are - appended unstyled. - - Args: - *message: One or more message parts to display. - """ - emit( - "==>", - "blue", - *message, - enabled=obj.get("verbose", False) and not obj.get("quiet", False), - ) - - -@click.pass_obj -def debug(obj: dict[str, bool], *message: str) -> None: - """Print a formatted debug message to stdout in magenta. - - Only prints when the DEBUG environment variable is set to a truthy - value (1, true, yes). The first argument is rendered in bold, - subsequent arguments are appended unstyled. - - Args: - *message: One or more message parts to display. - """ - emit("==>", "magenta", *message, enabled=obj.get("debug", False)) - - def print_version( - ctx: click.Context, - param: click.Parameter | None, - value: bool, + ctx: click.Context, param: click.Parameter | None, value: bool ) -> None: """Print script metadata (name, version, description, author, dates) and exit. @@ -202,11 +80,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.""") ctx.exit() -############################################################################### -# MAIN EXECUTION -############################################################################### - - @click.pass_context def run_command( ctx: click.Context, command: list[str] @@ -226,10 +99,7 @@ def run_command( debug("Running command", " ".join(command)) try: return subprocess.run( - command, - capture_output=True, - encoding="utf-8", - check=True, + 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}") @@ -242,8 +112,8 @@ 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. + 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. @@ -321,8 +191,7 @@ def fetch_repositories( if all_archives: borgmatic_info = run_command(info_command) repositories = cast( - list[types.BorgInfoReturnType], - load_json(borgmatic_info.stdout), + list[types.BorgInfoReturnType], load_json(borgmatic_info.stdout) ) debug(f"Fetched {len(repositories)} repositories") return repositories, extract_archive_counts(repositories) @@ -345,8 +214,8 @@ def collect_repository_metrics( """Collect and set all Prometheus metrics for a single 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. + archive metrics. When all_archives is True, also collects metrics for every + individual archive by calling _collect_archive_metrics. Args: repository: Repository info as returned by borgmatic. @@ -394,10 +263,7 @@ def collect_repository_metrics( warn("No archives found for repository:", repository_metadata["label"]) return - latest_archive = max( - repository["archives"], - key=lambda archive: archive["start"], - ) + latest_archive = max(repository["archives"], key=lambda archive: archive["start"]) debug(f"Latest archive {latest_archive['name']}:", latest_archive["start"]) metrics.LATEST_ARCHIVE_DURATION.labels(**repository_labels).set( @@ -419,8 +285,7 @@ def collect_repository_metrics( def collect_archive_metrics( - archive: types.BorgArchive, - repository_labels: types.RepositoryLabels, + archive: types.BorgArchive, repository_labels: types.RepositoryLabels ) -> None: """Collect and set Prometheus metrics for a single archive. @@ -450,15 +315,7 @@ def collect_archive_metrics( @click.command( - context_settings={"help_option_names": ("-h", "--help", "-?")}, - formatter_settings=click.HelpExtraFormatter.settings( - theme=click.HelpExtraTheme.light(), - ), - params=[], -) -@click.color_option( - "--color/--no-color", - help="Force/Disable colored output.", + context_settings={"help_option_names": ("-h", "--help", "-?")}, params=[] ) @click.option( "--borgmatic-bin", @@ -471,6 +328,7 @@ def collect_archive_metrics( path_type=Path, ), default="/usr/bin/borgmatic", + help="Path to the borgmatic executable.", ) @click.option( "--borgmatic-config", @@ -496,10 +354,36 @@ def collect_archive_metrics( path_type=Path, ), default="/var/lib/prometheus/node-exporter", + help="Directory where the Prometheus textfile collector file is written.", +) +@click.option( + "--all-archives", + "all_archives_flag", + is_flag=True, + default=False, + help=( + "Collect metrics for all archives instead of only the latest archive. " + "This can significantly increase runtime and Prometheus storage usage." + ), +) +@click.option( + "-q", + "--quiet", + "quiet_flag", + is_flag=True, + default=False, + help="Suppress normal output.", +) +@click.option( + "-v", + "--verbose", + "verbose_flag", + is_flag=True, + default=False, + help="Enable verbose output.", ) -@click.option("--all-archives", "all_archives_flag", is_flag=True, default=False) -@click.option("-q", "--quiet", "quiet_flag", is_flag=True, default=False) -@click.option("-v", "--verbose", "verbose_flag", is_flag=True, default=False) +@click.option("--color", cls=FlexibleColorOption) +@click.no_color_option() @click.option( "--version", callback=print_version, @@ -521,13 +405,13 @@ 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 + Queries borgmatic for repository and archive statistics and writes them to + 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. + --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. diff --git a/prometheus_borgmatic_exporter/cli.py b/prometheus_borgmatic_exporter/cli.py new file mode 100644 index 0000000..cc6f374 --- /dev/null +++ b/prometheus_borgmatic_exporter/cli.py @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> +# +# SPDX-License-Identifier: BSD-3-Clause + +import click_extra as click + + +class FlexibleColorOption(click.ColorOption): + """Allow the color option value to be passed with or without ``=``.""" + + _gnu_optional_value = False + + +def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None: + """Render a styled prefix and message to stdout. + + Internal helper used by error(), msg(), warn(), verbose(), and debug() + to avoid duplicating the Click styling and echo logic. The first message + argument is rendered in bold, subsequent arguments are appended unstyled. + + Args: + prefix: The prefix string shown before the message (e.g. "==>", "==> ERROR:"). + color: A Click-compatible color name (e.g. "red", "green") applied to the prefix. + *message: One or more message parts to display. At least one is required. + enabled: If False, the function returns immediately without printing. + Defaults to True. + + Raises: + TypeError: If no message arguments are provided. + """ + if not message: + raise TypeError("emit() missing 1 required positional argument: 'message'") + + if not enabled: + return + + click.echo( + " ".join( + [ + click.style(prefix, fg=color, bold=True), + click.style(message[0], bold=True), + *message[1:], + ] + ) + ) + + +def error(*message: str) -> None: + """Print a formatted error message to stdout in red. + + The first argument is rendered in bold, subsequent arguments are appended + unstyled. Always prints regardless of quiet or verbose flags. + + Args: + *message: One or more message parts to display. + """ + emit("==> ERROR:", "red", *message) + + +@click.pass_obj +def msg(obj: dict[str, bool], *message: str) -> None: + """Print a formatted informational message to stdout in green. + + Suppressed when the quiet flag is set. The first argument is rendered + in bold, subsequent arguments are appended unstyled. + + Args: + *message: One or more message parts to display. + """ + emit("==>", "green", *message, enabled=not obj.get("quiet", False)) + + +@click.pass_obj +def warn(obj: dict[str, bool], *message: str) -> None: + """Print a formatted warning message to stdout in yellow. + + Suppressed when the quiet flag is set. The first argument is rendered + in bold, subsequent arguments are appended unstyled. + + Args: + *message: One or more message parts to display. + """ + emit("==>", "yellow", *message, enabled=not obj.get("quiet", False)) + + +@click.pass_obj +def verbose(obj: dict[str, bool], *message: str) -> None: + """Print a formatted verbose message to stdout in blue. + + Only prints when the verbose flag is set and the quiet flag is not. + The first argument is rendered in bold, subsequent arguments are + appended unstyled. + + Args: + *message: One or more message parts to display. + """ + emit( + "==>", + "blue", + *message, + enabled=obj.get("verbose", False) and not obj.get("quiet", False), + ) + + +@click.pass_obj +def debug(obj: dict[str, bool], *message: str) -> None: + """Print a formatted debug message to stdout in magenta. + + Only prints when the DEBUG environment variable is set to a truthy + value (1, true, yes). The first argument is rendered in bold, + subsequent arguments are appended unstyled. + + Args: + *message: One or more message parts to display. + """ + emit("==>", "magenta", *message, enabled=obj.get("debug", False)) |
