First version
This commit is contained in:
parent
252b9aee6f
commit
fb767eba1e
5 changed files with 280 additions and 2 deletions
57
README.md
57
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).
|
||||
|
|
0
qbittorrent_exporter/__init__.py
Normal file
0
qbittorrent_exporter/__init__.py
Normal file
202
qbittorrent_exporter/exporter.py
Normal file
202
qbittorrent_exporter/exporter.py
Normal file
|
@ -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")
|
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
|||
[metadata]
|
||||
description-file = README.md
|
21
setup.py
Normal file
21
setup.py
Normal file
|
@ -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',
|
||||
]
|
||||
}
|
||||
)
|
Loading…
Reference in a new issue