diff --git a/README.md b/README.md index 5fc5d48..979c5be 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ -# prometheus-qbittorrent-exporter -A prometheus exporter for qbitorrent +# Prometheus qBittorrent exporter + +A prometheus exporter for qBitorrent. Get metrics from a server and offers them in a prometheus format. + + +## How to use it + +You can install this exporter with the following command: + +```bash +pip3 install prometheus-qbittorrent-exporter +``` + +Then you can run it with + +``` +qbittorrent-exporter +``` + +Another option is run it in a docker container. + +``` +docker run esanchezm/prometheus-qbittorrent-exporter +``` + +The application reads configuration using environment variables: + +| Environment variable | Default | Description | +| -------------------- | ------------- | ----------- | +| `QBITTORRENT_HOST` | | qbittorrent server hostname | +| `QBITTORRENT_PORT` | | qbittorrent server port | +| `QBITTORRENT_USER` | `""` | qbittorrent username | +| `QBITTORRENT_PASS` | `""` | qbittorrent password | +| `EXPORTER_PORT` | `8000` | Exporter listening port | +| `EXPORTER_LOG_LEVEL` | `INFO` | Log level. One of: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | + + +## Metrics + +These are the metrics this program exports: + + +| 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 | +| `torrents_count` | gauge | Number of torrents for each `category` and `status`. Example: `torrents_count{category="movies",status="downloading"}`| + +## License + +This software is released under the [GPLv3 license](LICENSE). diff --git a/qbittorrent_exporter/__init__.py b/qbittorrent_exporter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qbittorrent_exporter/exporter.py b/qbittorrent_exporter/exporter.py new file mode 100644 index 0000000..8405895 --- /dev/null +++ b/qbittorrent_exporter/exporter.py @@ -0,0 +1,202 @@ +from httplib2 import Http +import time +import os +import signal +import faulthandler +from qbittorrentapi import Client, TorrentStates +from prometheus_client import start_http_server +from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY +import logging +from pythonjsonlogger import jsonlogger + + +# Enable dumps on stderr in case of segfault +faulthandler.enable() +logger = None + + +class QbittorrentMetricsCollector(): + TORRENT_STATUSES = [ + "downloading", + "uploading", + "complete", + "checking", + "errored", + "paused", + ] + + def __init__(self, config): + self.config = config + self.torrents = None + self.client = Client( + host=config["host"], + port=config["port"], + username=config["username"], + password=config["password"], + ) + + def collect(self): + try: + self.torrents = self.client.torrents.info() + except Exception as e: + logger.error(f"Couldn't get server info: {e}") + + metrics = 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()) + else: + prom_metric = GaugeMetricFamily(name, help_text, labels=labels.keys()) + prom_metric.add_metric(value=value, labels=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()) + + return metrics + + def get_qbittorrent_status_metrics(self): + # Fetch data from API + try: + response = self.client.transfer.info + version = self.client.app.version + self.torrents = self.client.torrents.info() + except Exception as e: + logger.error(f"Couldn't get server info: {e}") + response = None + version = "" + + return [ + { + "name": "up", + "value": response is None, + "labels": {"version": version}, + "help": "Whether if server is alive or not", + }, + { + "name": "connected", + "value": response.get("connection_status", "") == "connected", + "help": "Whether if server is connected or not", + }, + { + "name": "firewalled", + "value": response.get("connection_status", "") == "firewalled", + "help": "Whether if server is under a firewall or not", + }, + { + "name": "dht_nodes", + "value": response.get("dht_nodes", 0), + "help": "DHT nodes connected to", + }, + { + "name": "dl_info_data", + "value": response.get("dl_info_data", 0), + "help": "Data downloaded this session (bytes)", + "type": "counter" + }, + { + "name": "up_info_data", + "value": response.get("up_info_data", 0), + "help": "Data uploaded this session (bytes)", + "type": "counter" + }, + ] + + def get_qbittorrent_torrent_tags_metrics(self): + try: + categories = self.client.torrent_categories.categories + except Exception as e: + logger.error(f"Couldn't fetch categories: {e}") + categories = None + + if not categories: + return [] + + if not self.torrents: + return [] + + metrics = [] + for category in categories: + category_torrents = [t for t in self.torrents if t['category'] == 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": "torrents_count", + "value": len(status_torrents), + "labels": { + "status": status, + "category": category, + }, + "help": f"Number of torrents in status {status} under category {category}" + }) + + return metrics + + +class SignalHandler(): + def __init__(self): + self.shutdown = False + + # 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.shutdown + + def _on_signal_received(self, signal, frame): + logger.info("Exporter is shutting down") + self.shutdown = True + + +def main(): + config = { + "host": os.environ.get("QBITTORRENT_HOST", ""), + "port": os.environ.get("QBITTORRENT_PORT", ""), + "username": os.environ.get("QBITTORRENT_USER", ""), + "password": os.environ.get("QBITTORRENT_PASS", ""), + "exporter_port": int(os.environ.get("EXPORTER_PORT", "8000")), + "log_level": os.environ.get("EXPORTER_LOG_LEVEL", "INFO") + } + + # Register signal handler + signal_handler = SignalHandler() + + # Init logger + logHandler = logging.StreamHandler() + formatter = jsonlogger.JsonFormatter( + "%(asctime) %(levelname) %(message)", + datefmt="%Y-%m-%d %H:%M:%S" + ) + logHandler.setFormatter(formatter) + logger = logging.getLogger() + logger.addHandler(logHandler) + logger.setLevel(config["log_level"]) + + # Register our custom collector + logger.info("Exporter is starting up") + REGISTRY.register(QbittorrentMetricsCollector(config)) + + # Start server + start_http_server(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") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..27da2db --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup + +setup( + name='prometheus-qbittorrent-exporter', + packages=['qbittorrent_exporter'], + version='1.0.0', + 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/spreaker/prometheus-qbittorrent-exporter/archive/1.0.0.tar.gz', + keywords=['prometheus', 'qbittorrent'], + classifiers=[], + python_requires='>=3', + install_requires=['qbittorrent-api==2020.9.9', 'prometheus_client==0.8.0', 'python-json-logger==0.1.5'], + entry_points={ + 'console_scripts': [ + 'qbittorrent-exporter=qbittorrent_exporter.exporter:main', + ] + } +)