Merge pull request #21 from joelheaps/python311-upgrade

Python 3.11 upgrade and code refactor
This commit is contained in:
Esteban Sánchez 2023-11-20 10:24:05 +01:00 committed by GitHub
commit 6d2d7e666a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 385 additions and 149 deletions

6
.gitignore vendored
View File

@ -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

View File

@ -1,4 +1,4 @@
FROM python:3.8-alpine3.17 FROM python:3.11-alpine
# Install package # Install package
WORKDIR /code WORKDIR /code

View File

@ -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
View 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
View 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
View 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"

View File

@ -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 [
for status in self.TORRENT_STATUSES: torrent
status_prop = f"is_{status}" for torrent in torrents
status_torrents = [ if torrent["category"] == category
t for t in category_torrents if getattr(TorrentStates, status_prop).fget(TorrentStates(t['state'])) or (category == "Uncategorized" and torrent["category"] == "")
] ]
metrics.append({
"name": f"{self.config['metrics_prefix']}_torrents_count", def _filter_torrents_by_state(
"value": len(status_torrents), self, state: TorrentStates, torrents: list[dict]
"labels": { ) -> list[dict]:
"status": status, """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, "category": category,
}, },
"help": f"Number of torrents in status {status} under 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 = { config = get_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()

View File

@ -1,2 +0,0 @@
[metadata]
description-file = README.md

View File

@ -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',
]
}
)