diff --git a/.gitignore b/.gitignore index b6e4761..97b792c 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,9 @@ dmypy.json # Pyre type checker .pyre/ + +# Ignore config.env +config.env + +# Ignore pdm local files +.pdm-python \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b094436..9b21d15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine3.17 +FROM python:3.11-alpine # Install package WORKDIR /code diff --git a/README.md b/README.md index f1210d1..97d2a1e 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ These are the metrics this program exports, assuming the `METRICS_PREFIX` is `qb | Metric name | Type | Description | | --------------------------------------------------- | -------- | ---------------- | -| `qbittorrent_up` | gauge | Whether if the qBittorrent server is answering requests from this exporter. A `version` label with the server version is added | -| `qbittorrent_connected` | gauge | Whether if the qBittorrent server is connected to the Bittorrent network. | -| `qbittorrent_firewalled` | gauge | Whether if the qBittorrent server is connected to the Bittorrent network but is behind a firewall. | -| `qbittorrent_dht_nodes` | gauge | Number of DHT nodes connected to | -| `qbittorrent_dl_info_data` | counter | Data downloaded since the server started, in bytes | -| `qbittorrent_up_info_data` | counter | Data uploaded since the server started, in bytes | +| `qbittorrent_up` | gauge | Whether the qBittorrent server is answering requests from this exporter. A `version` label with the server version is added. | +| `qbittorrent_connected` | gauge | Whether the qBittorrent server is connected to the Bittorrent network. | +| `qbittorrent_firewalled` | gauge | Whether the qBittorrent server is connected to the Bittorrent network but is behind a firewall. | +| `qbittorrent_dht_nodes` | gauge | Number of DHT nodes connected to. | +| `qbittorrent_dl_info_data` | counter | Data downloaded since the server started, in bytes. | +| `qbittorrent_up_info_data` | counter | Data uploaded since the server started, in bytes. | | `qbittorrent_torrents_count` | gauge | Number of torrents for each `category` and `status`. Example: `qbittorrent_torrents_count{category="movies",status="downloading"}`| ## Screenshot diff --git a/config.env.example b/config.env.example new file mode 100644 index 0000000..55bbd2a --- /dev/null +++ b/config.env.example @@ -0,0 +1,6 @@ +QBITTORRENT_HOST=localhost +QBITTORRENT_PORT=8080 +QBITTORRENT_USER=admin +QBITTORRENT_PASS=adminadmin +EXPORTER_PORT=8000 +METRICS_PREFIX=qbittorrent \ No newline at end of file diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..c97ef6b --- /dev/null +++ b/pdm.lock @@ -0,0 +1,135 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +cross_platform = true +static_urls = false +lock_version = "4.3" +content_hash = "sha256:ec00e4f386c7e3cac870ab101184e565ee290c82a8b3d759fb7ad2762d0366ba" + +[[package]] +name = "certifi" +version = "2023.7.22" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "idna" +version = "3.4" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "packaging" +version = "23.1" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "prometheus-client" +version = "0.17.1" +requires_python = ">=3.6" +summary = "Python client for the Prometheus monitoring system." +files = [ + {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, + {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, +] + +[[package]] +name = "python-json-logger" +version = "2.0.7" +requires_python = ">=3.6" +summary = "A python library adding a json log formatter" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + +[[package]] +name = "qbittorrent-api" +version = "2023.9.53" +summary = "Python client for qBittorrent v4.1+ Web API." +dependencies = [ + "packaging", + "requests>=2.16.0", + "six", + "urllib3>=1.24.2", +] +files = [ + {file = "qbittorrent-api-2023.9.53.tar.gz", hash = "sha256:fead1b2f55b1227ea088ea7d90b5022d94694bfd9dd9176beb5ad1c195d044ff"}, + {file = "qbittorrent_api-2023.9.53-py2.py3-none-any.whl", hash = "sha256:963ae59d16a9c4a9aa1714fb7f6799539dc2693136cdc0e377daab3612ca775a"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "urllib3" +version = "2.0.5" +requires_python = ">=3.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, + {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3e22cf5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "prometheus-qbittorrent-exporter" +version = "1.3.0" +description = "Prometheus exporter for qbittorrent" +authors = [ + {name = "Esteban Sanchez", email = "esteban.sanchez@gmail.com"}, +] +dependencies = [ + "prometheus-client>=0.17.1", + "python-json-logger>=2.0.7", + "qbittorrent-api>=2023.9.53", +] +requires-python = ">=3.11" +readme = "README.md" +keywords = ["prometheus", "qbittorrent"] +license = {text = "GPL-3.0"} +classifiers = [] + +[project.urls] +Homepage = "https://github.com/esanchezm/prometheus-qbittorrent-exporter" +Downloads = "https://github.com/esanchezm/prometheus-qbittorrent-exporter/archive/1.3.0.tar.gz" + +[project.scripts] +qbittorrent-exporter = "qbittorrent_exporter.exporter:main" + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" diff --git a/qbittorrent_exporter/exporter.py b/qbittorrent_exporter/exporter.py index 5e69547..bae7608 100644 --- a/qbittorrent_exporter/exporter.py +++ b/qbittorrent_exporter/exporter.py @@ -3,67 +3,89 @@ import os import sys import signal import faulthandler -from attrdict import AttrDict from qbittorrentapi import Client, TorrentStates -from qbittorrentapi.exceptions import APIConnectionError from prometheus_client import start_http_server from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY import logging from pythonjsonlogger import jsonlogger - +from enum import StrEnum, auto +from typing import Iterable, Any +from dataclasses import dataclass, field # Enable dumps on stderr in case of segfault faulthandler.enable() logger = logging.getLogger() -class QbittorrentMetricsCollector(): - TORRENT_STATUSES = [ - "checking", - "complete", - "downloading", - "errored", - "paused", - "uploading", - ] +class MetricType(StrEnum): + """ + Represents possible metric types (used in this project). + """ - def __init__(self, config): + GAUGE = auto() + COUNTER = auto() + + +@dataclass +class Metric: + """ + Contains data and metadata about a single counter or gauge. + """ + + name: str + 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) -> None: self.config = config self.client = Client( host=config["host"], port=config["port"], username=config["username"], password=config["password"], - VERIFY_WEBUI_CERTIFICATE=config["verify_webui_certificate"] + VERIFY_WEBUI_CERTIFICATE=config["verify_webui_certificate"], ) - def collect(self): - metrics = self.get_qbittorrent_metrics() + def collect(self) -> Iterable[GaugeMetricFamily | CounterMetricFamily]: + """ + Yields Prometheus gauges and counters from metrics collected from qbittorrent. + """ + metrics: list[Metric] = self.get_qbittorrent_metrics() for metric in metrics: - name = metric["name"] - value = metric["value"] - help_text = metric.get("help", "") - labels = metric.get("labels", {}) - metric_type = metric.get("type", "gauge") - - if metric_type == "counter": - prom_metric = CounterMetricFamily(name, help_text, labels=labels.keys()) + if metric.metric_type == MetricType.COUNTER: + prom_metric = CounterMetricFamily( + metric.name, metric.help_text, labels=list(metric.labels.keys()) + ) else: - prom_metric = GaugeMetricFamily(name, help_text, labels=labels.keys()) - prom_metric.add_metric(value=value, labels=labels.values()) + prom_metric = GaugeMetricFamily( + metric.name, metric.help_text, labels=list(metric.labels.keys()) + ) + prom_metric.add_metric( + value=metric.value, labels=list(metric.labels.values()) + ) yield prom_metric - def get_qbittorrent_metrics(self): - metrics = [] - metrics.extend(self.get_qbittorrent_status_metrics()) - metrics.extend(self.get_qbittorrent_torrent_tags_metrics()) + 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): - response = {} - version = "" + def _get_qbittorrent_status_metrics(self) -> list[Metric]: + """ + Returns metrics about the state of the qbittorrent server. + """ + response: dict[str, Any] = {} + version: str = "" # Fetch data from API try: @@ -73,91 +95,145 @@ class QbittorrentMetricsCollector(): logger.error(f"Couldn't get server info: {e}") return [ - { - "name": f"{self.config['metrics_prefix']}_up", - "value": bool(response), - "labels": {"version": version}, - "help": "Whether if server is alive or not", - }, - { - "name": f"{self.config['metrics_prefix']}_connected", - "value": response.get("connection_status", "") == "connected", - "help": "Whether if server is connected or not", - }, - { - "name": f"{self.config['metrics_prefix']}_firewalled", - "value": response.get("connection_status", "") == "firewalled", - "help": "Whether if server is under a firewall or not", - }, - { - "name": f"{self.config['metrics_prefix']}_dht_nodes", - "value": response.get("dht_nodes", 0), - "help": "DHT nodes connected to", - }, - { - "name": f"{self.config['metrics_prefix']}_dl_info_data", - "value": response.get("dl_info_data", 0), - "help": "Data downloaded this session (bytes)", - "type": "counter" - }, - { - "name": f"{self.config['metrics_prefix']}_up_info_data", - "value": response.get("up_info_data", 0), - "help": "Data uploaded this session (bytes)", - "type": "counter" - }, + Metric( + name=f"{self.config['metrics_prefix']}_up", + value=bool(response), + labels={"version": version}, + help_text=( + "Whether the qBittorrent server is answering requests from this" + " exporter. A `version` label with the server version is added." + ), + ), + Metric( + name=f"{self.config['metrics_prefix']}_connected", + value=response.get("connection_status", "") == "connected", + labels={}, # no labels in the example + help_text=( + "Whether the qBittorrent server is connected to the Bittorrent" + " network." + ), + ), + Metric( + name=f"{self.config['metrics_prefix']}_firewalled", + value=response.get("connection_status", "") == "firewalled", + labels={}, # no labels in the example + help_text=( + "Whether the qBittorrent server is connected to the Bittorrent" + " network but is behind a firewall." + ), + ), + Metric( + name=f"{self.config['metrics_prefix']}_dht_nodes", + value=response.get("dht_nodes", 0), + labels={}, # no labels in the example + help_text="Number of DHT nodes connected to.", + ), + Metric( + name=f"{self.config['metrics_prefix']}_dl_info_data", + value=response.get("dl_info_data", 0), + labels={}, # no labels in the example + help_text="Data downloaded since the server started, in bytes.", + metric_type=MetricType.COUNTER, + ), + Metric( + name=f"{self.config['metrics_prefix']}_up_info_data", + value=response.get("up_info_data", 0), + labels={}, # no labels in the example + help_text="Data uploaded since the server started, in bytes.", + metric_type=MetricType.COUNTER, + ), ] - def get_qbittorrent_torrent_tags_metrics(self): + def _fetch_categories(self) -> dict: + """Fetches all categories in use from qbittorrent.""" try: - categories = self.client.torrent_categories.categories - torrents = self.client.torrents.info() + categories = dict(self.client.torrent_categories.categories) + for key, value in categories.items(): + categories[key] = dict(value) # type: ignore + return categories except Exception as e: - logger.error(f"Couldn't fetch torrent info: {e}") + logger.error(f"Couldn't fetch categories: {e}") + return {} + + def _fetch_torrents(self) -> list[dict]: + """Fetches torrents from qbittorrent""" + try: + return [dict(_attr_dict) for _attr_dict in self.client.torrents.info()] + except Exception as e: + logger.error(f"Couldn't fetch torrents: {e}") return [] - metrics = [] - categories.Uncategorized = AttrDict({'name': 'Uncategorized', 'savePath': ''}) - for category in categories: - category_torrents = [t for t in torrents if t['category'] == category or (category == "Uncategorized" and t['category'] == "")] + def _filter_torrents_by_category( + self, category: str, torrents: list[dict] + ) -> list[dict]: + """Filters torrents by the given category.""" + return [ + torrent + for torrent in torrents + if torrent["category"] == category + or (category == "Uncategorized" and torrent["category"] == "") + ] - for status in self.TORRENT_STATUSES: - status_prop = f"is_{status}" - status_torrents = [ - t for t in category_torrents if getattr(TorrentStates, status_prop).fget(TorrentStates(t['state'])) - ] - metrics.append({ - "name": f"{self.config['metrics_prefix']}_torrents_count", - "value": len(status_torrents), - "labels": { - "status": status, - "category": category, - }, - "help": f"Number of torrents in status {status} under category {category}" - }) + def _filter_torrents_by_state( + self, state: TorrentStates, torrents: list[dict] + ) -> list[dict]: + """Filters torrents by the given state.""" + return [torrent for torrent in torrents if torrent["state"] == state.value] + + def _construct_metric(self, state: str, category: str, count: int) -> Metric: + """Constructs and returns a metric object with a torrent count and appropriate + labels.""" + return Metric( + name=f"{self.config['metrics_prefix']}_torrents_count", + value=count, + labels={ + "status": state, + "category": category, + }, + help_text=f"Number of torrents in status {state} under category {category}", + ) + + def _get_qbittorrent_torrent_tags_metrics(self) -> list[Metric]: + categories = self._fetch_categories() + torrents = self._fetch_torrents() + + metrics: list[Metric] = [] + categories["Uncategorized"] = {"name": "Uncategorized", "savePath": ""} + + for category in categories: + category_torrents = self._filter_torrents_by_category(category, torrents) + for state in TorrentStates: + state_torrents = self._filter_torrents_by_state( + state, category_torrents + ) + metric = self._construct_metric( + state.value, category, len(state_torrents) + ) + metrics.append(metric) return metrics -class SignalHandler(): +class ShutdownSignalHandler: def __init__(self): - self.shutdownCount = 0 + self.shutdown_count: int = 0 # Register signal handler signal.signal(signal.SIGINT, self._on_signal_received) signal.signal(signal.SIGTERM, self._on_signal_received) def is_shutting_down(self): - return self.shutdownCount > 0 + return self.shutdown_count > 0 def _on_signal_received(self, signal, frame): - if self.shutdownCount > 1: + if self.shutdown_count > 1: logger.warn("Forcibly killing exporter") sys.exit(1) logger.info("Exporter is shutting down") - self.shutdownCount += 1 + self.shutdown_count += 1 -def get_config_value(key, default=""): + +def _get_config_value(key: str, default: str = "") -> str: input_path = os.environ.get("FILE__" + key, None) if input_path is not None: try: @@ -169,51 +245,64 @@ def get_config_value(key, default=""): return os.environ.get(key, default) +def get_config() -> dict: + """Loads all config values.""" + return { + "host": _get_config_value("QBITTORRENT_HOST", ""), + "port": _get_config_value("QBITTORRENT_PORT", ""), + "username": _get_config_value("QBITTORRENT_USER", ""), + "password": _get_config_value("QBITTORRENT_PASS", ""), + "exporter_port": int(_get_config_value("EXPORTER_PORT", "8000")), + "log_level": _get_config_value("EXPORTER_LOG_LEVEL", "INFO"), + "metrics_prefix": _get_config_value("METRICS_PREFIX", "qbittorrent"), + "verify_webui_certificate": ( + _get_config_value("VERIFY_WEBUI_CERTIFICATE", "True") == "True" + ), + } + + def main(): # Init logger so it can be used logHandler = logging.StreamHandler() formatter = jsonlogger.JsonFormatter( - "%(asctime) %(levelname) %(message)", - datefmt="%Y-%m-%d %H:%M:%S" + "%(asctime) %(levelname) %(message)", datefmt="%Y-%m-%d %H:%M:%S" ) logHandler.setFormatter(formatter) logger.addHandler(logHandler) - logger.setLevel("INFO") # default until config is loaded + logger.setLevel("INFO") # default until config is loaded + + config = get_config() - config = { - "host": get_config_value("QBITTORRENT_HOST", ""), - "port": get_config_value("QBITTORRENT_PORT", ""), - "username": get_config_value("QBITTORRENT_USER", ""), - "password": get_config_value("QBITTORRENT_PASS", ""), - "exporter_port": int(get_config_value("EXPORTER_PORT", "8000")), - "log_level": get_config_value("EXPORTER_LOG_LEVEL", "INFO"), - "metrics_prefix": get_config_value("METRICS_PREFIX", "qbittorrent"), - "verify_webui_certificate": get_config_value("VERIFY_WEBUI_CERTIFICATE", "True") == "True", - } # set level once config has been loaded logger.setLevel(config["log_level"]) # Register signal handler - signal_handler = SignalHandler() + signal_handler = ShutdownSignalHandler() if not config["host"]: - logger.error("No host specified, please set QBITTORRENT_HOST environment variable") + logger.error( + "No host specified, please set QBITTORRENT_HOST environment variable" + ) sys.exit(1) if not config["port"]: - logger.error("No port specified, please set QBITTORRENT_PORT environment variable") + logger.error( + "No port specified, please set QBITTORRENT_PORT environment variable" + ) sys.exit(1) # 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"]) - logger.info( - f"Exporter listening on port {config['exporter_port']}" - ) + logger.info(f"Exporter listening on port {config['exporter_port']}") while not signal_handler.is_shutting_down(): time.sleep(1) logger.info("Exporter has shutdown") + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index 4512a87..0000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -from setuptools import setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name='prometheus-qbittorrent-exporter', - packages=['qbittorrent_exporter'], - version='1.3.0', - long_description=long_description, - long_description_content_type="text/markdown", - description='Prometheus exporter for qbittorrent', - author='Esteban Sanchez', - author_email='esteban.sanchez@gmail.com', - url='https://github.com/esanchezm/prometheus-qbittorrent-exporter', - download_url='https://github.com/esanchezm/prometheus-qbittorrent-exporter/archive/1.3.0.tar.gz', - keywords=['prometheus', 'qbittorrent'], - classifiers=[], - python_requires='>=3,<3.10', - install_requires=['attrdict==2.0.1', 'qbittorrent-api==2023.4.47', 'prometheus_client==0.16.0', 'python-json-logger==2.0.2'], - entry_points={ - 'console_scripts': [ - 'qbittorrent-exporter=qbittorrent_exporter.exporter:main', - ] - } -)