mirror of
https://github.com/NohamR/prometheus-qbittorrent-exporter.git
synced 2025-05-24 00:59:28 +00:00
Merge pull request #21 from joelheaps/python311-upgrade
Python 3.11 upgrade and code refactor
This commit is contained in:
commit
6d2d7e666a
6
.gitignore
vendored
6
.gitignore
vendored
@ -127,3 +127,9 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Ignore config.env
|
||||||
|
config.env
|
||||||
|
|
||||||
|
# Ignore pdm local files
|
||||||
|
.pdm-python
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.8-alpine3.17
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
# Install package
|
# Install package
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
12
README.md
12
README.md
@ -53,12 +53,12 @@ These are the metrics this program exports, assuming the `METRICS_PREFIX` is `qb
|
|||||||
|
|
||||||
| Metric name | Type | Description |
|
| 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_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 if the qBittorrent server is connected to the Bittorrent network. |
|
| `qbittorrent_connected` | gauge | Whether 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_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_dht_nodes` | gauge | Number of DHT nodes connected to. |
|
||||||
| `qbittorrent_dl_info_data` | counter | Data downloaded since the server started, in bytes |
|
| `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_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"}`|
|
| `qbittorrent_torrents_count` | gauge | Number of torrents for each `category` and `status`. Example: `qbittorrent_torrents_count{category="movies",status="downloading"}`|
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
6
config.env.example
Normal file
6
config.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
QBITTORRENT_HOST=localhost
|
||||||
|
QBITTORRENT_PORT=8080
|
||||||
|
QBITTORRENT_USER=admin
|
||||||
|
QBITTORRENT_PASS=adminadmin
|
||||||
|
EXPORTER_PORT=8000
|
||||||
|
METRICS_PREFIX=qbittorrent
|
135
pdm.lock
generated
Normal file
135
pdm.lock
generated
Normal file
@ -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"},
|
||||||
|
]
|
28
pyproject.toml
Normal file
28
pyproject.toml
Normal file
@ -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"
|
@ -3,67 +3,89 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import faulthandler
|
import faulthandler
|
||||||
from attrdict import AttrDict
|
|
||||||
from qbittorrentapi import Client, TorrentStates
|
from qbittorrentapi import Client, TorrentStates
|
||||||
from qbittorrentapi.exceptions import APIConnectionError
|
|
||||||
from prometheus_client import start_http_server
|
from prometheus_client import start_http_server
|
||||||
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
|
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
|
||||||
import logging
|
import logging
|
||||||
from pythonjsonlogger import jsonlogger
|
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
|
# Enable dumps on stderr in case of segfault
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class QbittorrentMetricsCollector():
|
class MetricType(StrEnum):
|
||||||
TORRENT_STATUSES = [
|
"""
|
||||||
"checking",
|
Represents possible metric types (used in this project).
|
||||||
"complete",
|
"""
|
||||||
"downloading",
|
|
||||||
"errored",
|
|
||||||
"paused",
|
|
||||||
"uploading",
|
|
||||||
]
|
|
||||||
|
|
||||||
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.config = config
|
||||||
self.client = Client(
|
self.client = Client(
|
||||||
host=config["host"],
|
host=config["host"],
|
||||||
port=config["port"],
|
port=config["port"],
|
||||||
username=config["username"],
|
username=config["username"],
|
||||||
password=config["password"],
|
password=config["password"],
|
||||||
VERIFY_WEBUI_CERTIFICATE=config["verify_webui_certificate"]
|
VERIFY_WEBUI_CERTIFICATE=config["verify_webui_certificate"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def collect(self):
|
def collect(self) -> Iterable[GaugeMetricFamily | CounterMetricFamily]:
|
||||||
metrics = self.get_qbittorrent_metrics()
|
"""
|
||||||
|
Yields Prometheus gauges and counters from metrics collected from qbittorrent.
|
||||||
|
"""
|
||||||
|
metrics: list[Metric] = self.get_qbittorrent_metrics()
|
||||||
|
|
||||||
for metric in metrics:
|
for metric in metrics:
|
||||||
name = metric["name"]
|
if metric.metric_type == MetricType.COUNTER:
|
||||||
value = metric["value"]
|
prom_metric = CounterMetricFamily(
|
||||||
help_text = metric.get("help", "")
|
metric.name, metric.help_text, labels=list(metric.labels.keys())
|
||||||
labels = metric.get("labels", {})
|
)
|
||||||
metric_type = metric.get("type", "gauge")
|
|
||||||
|
|
||||||
if metric_type == "counter":
|
|
||||||
prom_metric = CounterMetricFamily(name, help_text, labels=labels.keys())
|
|
||||||
else:
|
else:
|
||||||
prom_metric = GaugeMetricFamily(name, help_text, labels=labels.keys())
|
prom_metric = GaugeMetricFamily(
|
||||||
prom_metric.add_metric(value=value, labels=labels.values())
|
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
|
yield prom_metric
|
||||||
|
|
||||||
def get_qbittorrent_metrics(self):
|
def get_qbittorrent_metrics(self) -> list[Metric]:
|
||||||
metrics = []
|
"""
|
||||||
metrics.extend(self.get_qbittorrent_status_metrics())
|
Calls and combines qbittorrent state metrics with torrent metrics.
|
||||||
metrics.extend(self.get_qbittorrent_torrent_tags_metrics())
|
"""
|
||||||
|
metrics: list[Metric] = []
|
||||||
|
metrics.extend(self._get_qbittorrent_status_metrics())
|
||||||
|
metrics.extend(self._get_qbittorrent_torrent_tags_metrics())
|
||||||
|
|
||||||
return metrics
|
return metrics
|
||||||
|
|
||||||
def get_qbittorrent_status_metrics(self):
|
def _get_qbittorrent_status_metrics(self) -> list[Metric]:
|
||||||
response = {}
|
"""
|
||||||
version = ""
|
Returns metrics about the state of the qbittorrent server.
|
||||||
|
"""
|
||||||
|
response: dict[str, Any] = {}
|
||||||
|
version: str = ""
|
||||||
|
|
||||||
# Fetch data from API
|
# Fetch data from API
|
||||||
try:
|
try:
|
||||||
@ -73,91 +95,145 @@ class QbittorrentMetricsCollector():
|
|||||||
logger.error(f"Couldn't get server info: {e}")
|
logger.error(f"Couldn't get server info: {e}")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
Metric(
|
||||||
"name": f"{self.config['metrics_prefix']}_up",
|
name=f"{self.config['metrics_prefix']}_up",
|
||||||
"value": bool(response),
|
value=bool(response),
|
||||||
"labels": {"version": version},
|
labels={"version": version},
|
||||||
"help": "Whether if server is alive or not",
|
help_text=(
|
||||||
},
|
"Whether the qBittorrent server is answering requests from this"
|
||||||
{
|
" exporter. A `version` label with the server version is added."
|
||||||
"name": f"{self.config['metrics_prefix']}_connected",
|
),
|
||||||
"value": response.get("connection_status", "") == "connected",
|
),
|
||||||
"help": "Whether if server is connected or not",
|
Metric(
|
||||||
},
|
name=f"{self.config['metrics_prefix']}_connected",
|
||||||
{
|
value=response.get("connection_status", "") == "connected",
|
||||||
"name": f"{self.config['metrics_prefix']}_firewalled",
|
labels={}, # no labels in the example
|
||||||
"value": response.get("connection_status", "") == "firewalled",
|
help_text=(
|
||||||
"help": "Whether if server is under a firewall or not",
|
"Whether the qBittorrent server is connected to the Bittorrent"
|
||||||
},
|
" network."
|
||||||
{
|
),
|
||||||
"name": f"{self.config['metrics_prefix']}_dht_nodes",
|
),
|
||||||
"value": response.get("dht_nodes", 0),
|
Metric(
|
||||||
"help": "DHT nodes connected to",
|
name=f"{self.config['metrics_prefix']}_firewalled",
|
||||||
},
|
value=response.get("connection_status", "") == "firewalled",
|
||||||
{
|
labels={}, # no labels in the example
|
||||||
"name": f"{self.config['metrics_prefix']}_dl_info_data",
|
help_text=(
|
||||||
"value": response.get("dl_info_data", 0),
|
"Whether the qBittorrent server is connected to the Bittorrent"
|
||||||
"help": "Data downloaded this session (bytes)",
|
" network but is behind a firewall."
|
||||||
"type": "counter"
|
),
|
||||||
},
|
),
|
||||||
{
|
Metric(
|
||||||
"name": f"{self.config['metrics_prefix']}_up_info_data",
|
name=f"{self.config['metrics_prefix']}_dht_nodes",
|
||||||
"value": response.get("up_info_data", 0),
|
value=response.get("dht_nodes", 0),
|
||||||
"help": "Data uploaded this session (bytes)",
|
labels={}, # no labels in the example
|
||||||
"type": "counter"
|
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:
|
try:
|
||||||
categories = self.client.torrent_categories.categories
|
categories = dict(self.client.torrent_categories.categories)
|
||||||
torrents = self.client.torrents.info()
|
for key, value in categories.items():
|
||||||
|
categories[key] = dict(value) # type: ignore
|
||||||
|
return categories
|
||||||
except Exception as e:
|
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 []
|
return []
|
||||||
|
|
||||||
metrics = []
|
def _filter_torrents_by_category(
|
||||||
categories.Uncategorized = AttrDict({'name': 'Uncategorized', 'savePath': ''})
|
self, category: str, torrents: list[dict]
|
||||||
for category in categories:
|
) -> list[dict]:
|
||||||
category_torrents = [t for t in torrents if t['category'] == category or (category == "Uncategorized" and t['category'] == "")]
|
"""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:
|
def _filter_torrents_by_state(
|
||||||
status_prop = f"is_{status}"
|
self, state: TorrentStates, torrents: list[dict]
|
||||||
status_torrents = [
|
) -> list[dict]:
|
||||||
t for t in category_torrents if getattr(TorrentStates, status_prop).fget(TorrentStates(t['state']))
|
"""Filters torrents by the given state."""
|
||||||
]
|
return [torrent for torrent in torrents if torrent["state"] == state.value]
|
||||||
metrics.append({
|
|
||||||
"name": f"{self.config['metrics_prefix']}_torrents_count",
|
def _construct_metric(self, state: str, category: str, count: int) -> Metric:
|
||||||
"value": len(status_torrents),
|
"""Constructs and returns a metric object with a torrent count and appropriate
|
||||||
"labels": {
|
labels."""
|
||||||
"status": status,
|
return Metric(
|
||||||
"category": category,
|
name=f"{self.config['metrics_prefix']}_torrents_count",
|
||||||
},
|
value=count,
|
||||||
"help": f"Number of torrents in status {status} under category {category}"
|
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
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
class SignalHandler():
|
class ShutdownSignalHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.shutdownCount = 0
|
self.shutdown_count: int = 0
|
||||||
|
|
||||||
# Register signal handler
|
# Register signal handler
|
||||||
signal.signal(signal.SIGINT, self._on_signal_received)
|
signal.signal(signal.SIGINT, self._on_signal_received)
|
||||||
signal.signal(signal.SIGTERM, self._on_signal_received)
|
signal.signal(signal.SIGTERM, self._on_signal_received)
|
||||||
|
|
||||||
def is_shutting_down(self):
|
def is_shutting_down(self):
|
||||||
return self.shutdownCount > 0
|
return self.shutdown_count > 0
|
||||||
|
|
||||||
def _on_signal_received(self, signal, frame):
|
def _on_signal_received(self, signal, frame):
|
||||||
if self.shutdownCount > 1:
|
if self.shutdown_count > 1:
|
||||||
logger.warn("Forcibly killing exporter")
|
logger.warn("Forcibly killing exporter")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
logger.info("Exporter is shutting down")
|
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)
|
input_path = os.environ.get("FILE__" + key, None)
|
||||||
if input_path is not None:
|
if input_path is not None:
|
||||||
try:
|
try:
|
||||||
@ -169,51 +245,64 @@ def get_config_value(key, default=""):
|
|||||||
return os.environ.get(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():
|
def main():
|
||||||
# Init logger so it can be used
|
# Init logger so it can be used
|
||||||
logHandler = logging.StreamHandler()
|
logHandler = logging.StreamHandler()
|
||||||
formatter = jsonlogger.JsonFormatter(
|
formatter = jsonlogger.JsonFormatter(
|
||||||
"%(asctime) %(levelname) %(message)",
|
"%(asctime) %(levelname) %(message)", datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
)
|
||||||
logHandler.setFormatter(formatter)
|
logHandler.setFormatter(formatter)
|
||||||
logger.addHandler(logHandler)
|
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
|
# set level once config has been loaded
|
||||||
logger.setLevel(config["log_level"])
|
logger.setLevel(config["log_level"])
|
||||||
|
|
||||||
# Register signal handler
|
# Register signal handler
|
||||||
signal_handler = SignalHandler()
|
signal_handler = ShutdownSignalHandler()
|
||||||
|
|
||||||
if not config["host"]:
|
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)
|
sys.exit(1)
|
||||||
if not config["port"]:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
# Register our custom collector
|
# Register our custom collector
|
||||||
logger.info("Exporter is starting up")
|
logger.info("Exporter is starting up")
|
||||||
REGISTRY.register(QbittorrentMetricsCollector(config))
|
REGISTRY.register(QbittorrentMetricsCollector(config)) # type: ignore
|
||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
start_http_server(config["exporter_port"])
|
start_http_server(config["exporter_port"])
|
||||||
logger.info(
|
logger.info(f"Exporter listening on port {config['exporter_port']}")
|
||||||
f"Exporter listening on port {config['exporter_port']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
while not signal_handler.is_shutting_down():
|
while not signal_handler.is_shutting_down():
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
logger.info("Exporter has shutdown")
|
logger.info("Exporter has shutdown")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
26
setup.py
26
setup.py
@ -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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
Loading…
x
Reference in New Issue
Block a user