diff --git a/pdm.lock b/pdm.lock index a2be35f..c97ef6b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -6,16 +6,7 @@ groups = ["default"] cross_platform = true static_urls = false lock_version = "4.3" -content_hash = "sha256:637d322b802d007082b71cf7d27382f3b6b5618434faf6b346358abe81fc2f7f" - -[[package]] -name = "attridict" -version = "0.0.8" -summary = "A dict implementation with support for easy and clean access of its values through attributes" -files = [ - {file = "attridict-0.0.8-py3-none-any.whl", hash = "sha256:8ee65af81f7762354e4514c443bbc04786a924c8e3e610c7883d2efbf323df6d"}, - {file = "attridict-0.0.8.tar.gz", hash = "sha256:23a17671b9439d36e2bdb0a69c09f033abab0900a9df178e0f89aa1b2c42c5cd"}, -] +content_hash = "sha256:ec00e4f386c7e3cac870ab101184e565ee290c82a8b3d759fb7ad2762d0366ba" [[package]] name = "certifi" diff --git a/pyproject.toml b/pyproject.toml index 7dd0006..3e22cf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,11 @@ dependencies = [ "prometheus-client>=0.17.1", "python-json-logger>=2.0.7", "qbittorrent-api>=2023.9.53", - "attridict>=0.0.8", ] requires-python = ">=3.11" readme = "README.md" keywords = ["prometheus", "qbittorrent"] -license = "GPL-3.0" +license = {text = "GPL-3.0"} classifiers = [] [project.urls] diff --git a/qbittorrent_exporter/exporter.py b/qbittorrent_exporter/exporter.py index 7e0f085..4d59637 100644 --- a/qbittorrent_exporter/exporter.py +++ b/qbittorrent_exporter/exporter.py @@ -3,7 +3,6 @@ import os import sys import signal import faulthandler -import attridict from qbittorrentapi import Client, TorrentStates from prometheus_client import start_http_server from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY @@ -18,7 +17,11 @@ faulthandler.enable() logger = logging.getLogger() -class TorrentStatus(StrEnum): +class TorrentStatuses(StrEnum): + """ + Represents possible torrent states. + """ + CHECKING = auto() COMPLETE = auto() ERRORED = auto() @@ -27,21 +30,29 @@ class TorrentStatus(StrEnum): class MetricType(StrEnum): + """ + Represents possible metric types (used in this project). + """ + GAUGE = auto() COUNTER = auto() @dataclass class Metric: + """ + Contains data and metadata about a single counter or gauge. + """ + name: str - value: int | float | bool - labels: dict[str, str] = field(default_factory={}) + value: Any + labels: dict[str, str] = field(default_factory=lambda: {}) # Default to empty dict help_text: str = "" metric_type: MetricType = MetricType.GAUGE class QbittorrentMetricsCollector: - def __init__(self, config: dict[str, str | int | bool]) -> None: + def __init__(self, config: dict) -> None: self.config = config self.client = Client( host=config["host"], @@ -52,28 +63,40 @@ class QbittorrentMetricsCollector: ) def collect(self) -> Iterable[GaugeMetricFamily | CounterMetricFamily]: + """ + Gets Metric objects representing the current state of qbittorrent and yields + Prometheus gauges. + """ metrics: list[Metric] = self.get_qbittorrent_metrics() for metric in metrics: if metric.metric_type == MetricType.COUNTER: prom_metric = CounterMetricFamily( - metric.name, metric.help_text, labels=metric.labels.keys() + metric.name, metric.help_text, labels=list(metric.labels.keys()) ) else: prom_metric = GaugeMetricFamily( - metric.name, metric.help_text, labels=metric.labels.keys() + metric.name, metric.help_text, labels=list(metric.labels.keys()) ) - prom_metric.add_metric(value=metric.value, labels=metric.labels.values()) + prom_metric.add_metric( + value=metric.value, labels=list(metric.labels.values()) + ) yield prom_metric def get_qbittorrent_metrics(self) -> list[Metric]: + """ + Calls and combines qbittorrent state metrics with torrent metrics. + """ metrics: list[Metric] = [] metrics.extend(self._get_qbittorrent_status_metrics()) metrics.extend(self._get_qbittorrent_torrent_tags_metrics()) return metrics - def _get_qbittorrent_status_metrics(self) -> list[dict]: + def _get_qbittorrent_status_metrics(self) -> list[Metric]: + """ + Returns metrics about the state of the qbittorrent server. + """ response: dict[str, Any] = {} version: str = "" @@ -147,22 +170,24 @@ class QbittorrentMetricsCollector: return [] metrics: list[Metric] = [] - categories.Uncategorized = attridict({"name": "Uncategorized", "savePath": ""}) + + # Match torrents to categories for category in categories: - category_torrents = [ - t - for t in torrents - if t["category"] == category - or (category == "Uncategorized" and t["category"] == "") + category_torrents: list = [ + torrent + for torrent in torrents + if torrent["category"] == category + or (category == "Uncategorized" and torrent["category"] == "") ] - for status in TorrentStatus: - status_prop = f"is_{status.value}" + # Match qbittorrentapi torrent state to local TorrenStatuses + for status in TorrentStatuses: + proposed_status: str = f"is_{status.value}" status_torrents = [ - t - for t in category_torrents - if getattr(TorrentStates, status_prop).fget( - TorrentStates(t["state"]) + torrent + for torrent in category_torrents + if getattr(TorrentStates, proposed_status).fget( + TorrentStates(torrent["state"]) ) ] metrics.append( @@ -255,7 +280,7 @@ def main(): # Register our custom collector logger.info("Exporter is starting up") - REGISTRY.register(QbittorrentMetricsCollector(config)) + REGISTRY.register(QbittorrentMetricsCollector(config)) # type: ignore # Start server start_http_server(config["exporter_port"])