mirror of
https://github.com/NohamR/prometheus-qbittorrent-exporter.git
synced 2026-01-09 07:38:18 +00:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75db331c55 | ||
|
|
d5c7cbc4a9 | ||
|
|
234a4cfffa | ||
|
|
4ad22a60da | ||
|
|
6c87b8eff9 | ||
|
|
e2a8508cae | ||
|
|
a6a4129daf | ||
|
|
723872bc15 | ||
|
|
cb78dd1fb7 | ||
|
|
8e08565d6e | ||
|
|
56a2792c2e | ||
|
|
1d154dd224 | ||
|
|
9ff35e48c3 | ||
|
|
a0929f84ba | ||
|
|
06f13a44a2 | ||
|
|
cbb2b6b4b1 | ||
|
|
e924e30cf8 | ||
|
|
1a04a83c4a | ||
|
|
03e4222043 | ||
|
|
03a9ac78c6 | ||
|
|
a60f3d23a3 | ||
|
|
dcaf2a6557 | ||
|
|
6d2d7e666a | ||
|
|
86b5feddf6 | ||
|
|
4385a0a78f | ||
|
|
71416a79ea | ||
|
|
330d8ec86a | ||
|
|
daed737f73 | ||
|
|
55e37eb814 | ||
|
|
ea4bdecdcf | ||
|
|
0ac406c476 | ||
|
|
3061eb1638 | ||
|
|
e56a713355 | ||
|
|
cd3911f53e | ||
|
|
50ca1e4162 | ||
|
|
45e8381d70 | ||
|
|
2f07f00592 | ||
|
|
e8c0141365 | ||
|
|
de1d0fa7d5 | ||
|
|
31f309c942 | ||
|
|
1fc4383a95 | ||
|
|
78cee60afd | ||
|
|
f4a5266125 | ||
|
|
40f721c447 | ||
|
|
a25ce3f7a2 | ||
|
|
2f32c69f50 | ||
|
|
5773684369 | ||
|
|
70ec628045 | ||
|
|
af0902b742 | ||
|
|
1d0707674a | ||
|
|
940b5c6242 | ||
|
|
f47ebe94d0 | ||
|
|
e1ed34147c | ||
|
|
1b2cf9065b | ||
|
|
5b2ed75850 | ||
|
|
bc8676ed30 | ||
|
|
fb12d50373 | ||
|
|
bb3695ece6 | ||
|
|
a9a3e5d1e6 | ||
|
|
6c885b5fa0 | ||
|
|
039f7a7ef4 | ||
|
|
8bdaa48c48 | ||
|
|
e8dd24f731 | ||
|
|
12dcad10d5 | ||
|
|
597307c230 | ||
|
|
a25005b6a0 | ||
|
|
cfe62f8115 | ||
|
|
b5d20e3fe6 | ||
|
|
0ff6a56f18 | ||
|
|
2d968bc8e8 | ||
|
|
8ab307a630 | ||
|
|
de00864f11 | ||
|
|
03d332979b | ||
|
|
1f6e8aa380 | ||
|
|
ef00d90e2f | ||
|
|
7f783f0b0a | ||
|
|
345888ef68 | ||
|
|
0e4c4a97ea | ||
|
|
d144f412ca | ||
|
|
9002a709b7 | ||
|
|
b91bff0f0c | ||
|
|
ed44d88c5b | ||
|
|
8f0a0381d3 | ||
|
|
b3e5e69f1f |
3
.coveragerc
Normal file
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[run]
|
||||||
|
relative_files = True
|
||||||
|
omit = tests/*
|
||||||
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
.coverage*
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
.pdm*
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
.vscode
|
||||||
|
build/
|
||||||
|
logo.png
|
||||||
|
tests
|
||||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
56
.github/workflows/docker.yml
vendored
56
.github/workflows/docker.yml
vendored
@@ -11,19 +11,63 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout GitHub Action'
|
- name: 'Checkout GitHub Action'
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker hub meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha
|
||||||
|
images: ${{ github.actor }}/prometheus-qbittorrent-exporter
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push docker
|
- name: Build and push docker to DockerHub
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: esanchezm/prometheus-qbittorrent-exporter:latest,esanchezm/prometheus-qbittorrent-exporter:$GITHUB_REF
|
platforms: linux/amd64,linux/arm64,linux/386
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: GHCR Docker meta
|
||||||
|
id: metaghcr
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha
|
||||||
|
images: ghcr.io/${{ github.actor }}/prometheus-qbittorrent-exporter
|
||||||
|
|
||||||
|
- name: Login to Github Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push docker to Github Container Registry
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/386
|
||||||
|
tags: ${{ steps.metaghcr.outputs.tags }}
|
||||||
|
labels: ${{ steps.metaghcr.outputs.labels }}
|
||||||
|
|||||||
59
.github/workflows/lint.yml
vendored
Normal file
59
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Run Unit Test via Pytest
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
checks: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Setup PDM
|
||||||
|
uses: pdm-project/setup-pdm@v3
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pdm install
|
||||||
|
- name: Lint with black
|
||||||
|
uses: psf/black@stable
|
||||||
|
with:
|
||||||
|
options: "--check --verbose"
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
pdm run pytest --junit-xml=test-results.xml
|
||||||
|
- name: Publish Test Results
|
||||||
|
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
test-results.xml
|
||||||
|
- name: Test with coverage
|
||||||
|
run: |
|
||||||
|
pdm run coverage run -m pytest -v -s
|
||||||
|
- name: Generate Coverage Report
|
||||||
|
run: |
|
||||||
|
pdm run coverage report -m
|
||||||
|
- name: Coverage comment
|
||||||
|
id: coverage_comment
|
||||||
|
uses: py-cov-action/python-coverage-comment-action@v3
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
- name: Store Pull Request comment to be posted
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true'
|
||||||
|
with:
|
||||||
|
name: python-coverage-comment-action
|
||||||
|
path: python-coverage-comment-action.txt
|
||||||
4
.github/workflows/pythonpublish.yml
vendored
4
.github/workflows/pythonpublish.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: '3.6'
|
python-version: '3.11'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@@ -22,5 +22,5 @@ jobs:
|
|||||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
python setup.py sdist
|
python -m build
|
||||||
twine upload dist/*
|
twine upload dist/*
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -127,3 +127,12 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Ignore config.env
|
||||||
|
config.env
|
||||||
|
|
||||||
|
# Ignore pdm local files
|
||||||
|
.pdm-python
|
||||||
|
|
||||||
|
# Ignore ruff files
|
||||||
|
.ruff_cache
|
||||||
|
|||||||
29
.pre-commit-config.yaml
Normal file
29
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
name: black
|
||||||
|
stages: [commit]
|
||||||
|
types: [python]
|
||||||
|
entry: pdm run black .
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
- id: ruff
|
||||||
|
name: ruff
|
||||||
|
stages: [commit]
|
||||||
|
types: [python]
|
||||||
|
entry: pdm run ruff .
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
fail_fast: true
|
||||||
|
- id: pytest
|
||||||
|
name: pytest
|
||||||
|
stages: [commit]
|
||||||
|
types: [python]
|
||||||
|
entry: pdm run pytest
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
fail_fast: true
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
FROM alpine:3.11
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
# Installing required packages
|
|
||||||
RUN apk add --update --no-cache \
|
|
||||||
python3
|
|
||||||
|
|
||||||
# Install package
|
# Install package
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -1,7 +1,12 @@
|
|||||||
# Prometheus qBittorrent exporter
|
# Prometheus qBittorrent exporter
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/esanchezm/prometheus-qbittorrent-exporter/master/logo.png" height="230">
|
||||||
|
</p>
|
||||||
|
|
||||||
A prometheus exporter for qBitorrent. Get metrics from a server and offers them in a prometheus format.
|
A prometheus exporter for qBitorrent. Get metrics from a server and offers them in a prometheus format.
|
||||||
|
|
||||||
|
      [](https://htmlpreview.github.io/?https://github.com/esanchezm/prometheus-qbittorrent-exporter/blob/python-coverage-comment-action-data/htmlcov/index.html)
|
||||||
|
|
||||||
## How to use it
|
## How to use it
|
||||||
|
|
||||||
@@ -20,20 +25,30 @@ qbittorrent-exporter
|
|||||||
Another option is run it in a docker container.
|
Another option is run it in a docker container.
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -e QBITTORRENT_PORT=8080 -e QBITTORRENT_HOST=myserver.local -p 8000:8000 esanchezm/prometheus-qbittorrent-exporter
|
docker run \
|
||||||
|
-e QBITTORRENT_PORT=8080 \
|
||||||
|
-e QBITTORRENT_HOST=myserver.local \
|
||||||
|
-p 8000:8000 \
|
||||||
|
ghcr.io/esanchezm/prometheus-qbittorrent-exporter
|
||||||
|
```
|
||||||
|
Add this to your prometheus.yml
|
||||||
|
```
|
||||||
|
- job_name: "qbittorrent_exporter"
|
||||||
|
static_configs:
|
||||||
|
- targets: ['yourqbittorrentexporter:port']
|
||||||
```
|
```
|
||||||
|
|
||||||
The application reads configuration using environment variables:
|
The application reads configuration using environment variables:
|
||||||
|
|
||||||
| Environment variable | Default | Description |
|
| Environment variable | Default | Description |
|
||||||
| -------------------- | ------------- | ----------- |
|
| -------------------------- | ------------- | ----------- |
|
||||||
| `QBITTORRENT_HOST` | | qbittorrent server hostname |
|
| `QBITTORRENT_HOST` | | qbittorrent server hostname |
|
||||||
| `QBITTORRENT_PORT` | | qbittorrent server port |
|
| `QBITTORRENT_PORT` | | qbittorrent server port |
|
||||||
| `QBITTORRENT_USER` | `""` | qbittorrent username |
|
| `QBITTORRENT_USER` | `""` | qbittorrent username |
|
||||||
| `QBITTORRENT_PASS` | `""` | qbittorrent password |
|
| `QBITTORRENT_PASS` | `""` | qbittorrent password |
|
||||||
| `EXPORTER_PORT` | `8000` | Exporter listening port |
|
| `EXPORTER_PORT` | `8000` | Exporter listening port |
|
||||||
| `EXPORTER_LOG_LEVEL` | `INFO` | Log level. One of: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
| `EXPORTER_LOG_LEVEL` | `INFO` | Log level. One of: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||||
| `METRICS_PREFIX` | `qbittorrent` | Prefix to add to all the metrics |
|
| `METRICS_PREFIX` | `qbittorrent` | Prefix to add to all the metrics |
|
||||||
|
| `VERIFY_WEBUI_CERTIFICATE` | `True` | Whether to verify SSL certificate when connecting to the qbittorrent server. Any other value but `True` will disable the verification |
|
||||||
|
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
@@ -43,14 +58,20 @@ 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[More info](./grafana/README.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is released under the [GPLv3 license](LICENSE).
|
This software is released under the [GPLv3 license](LICENSE).
|
||||||
|
|||||||
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
|
||||||
9
grafana/README.md
Normal file
9
grafana/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Grafana dashboard
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
To import the dashboard into your grafana, download the [dashboard.json](https://raw.githubusercontent.com/esanchezm/prometheus-qbittorrent-exporter/master/grafana/dashboard.json) file and import it into your server. Select your prometheus instance and that should be all.
|
||||||
|
|
||||||
|
## Screenshot
|
||||||
|
|
||||||
|

|
||||||
876
grafana/dashboard.json
Normal file
876
grafana/dashboard.json
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "grafana"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"target": {
|
||||||
|
"limit": 100,
|
||||||
|
"matchAny": false,
|
||||||
|
"tags": [],
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 5,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"text": "Offline"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"text": "Online"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"match": "null",
|
||||||
|
"result": {
|
||||||
|
"text": "Unknown"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "special"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "semi-dark-red",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "semi-dark-green",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"expr": "qbittorrent_up",
|
||||||
|
"format": "time_series",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Qbittorrent Status",
|
||||||
|
"transformations": [],
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"text": "Disconnected"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"text": "Connected"
|
||||||
|
},
|
||||||
|
"-1": {
|
||||||
|
"text": "Firewalled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "dark-red",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "semi-dark-green",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 3,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "qbittorrent_connected - qbittorrent_firewalled",
|
||||||
|
"format": "time_series",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "Status",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Bittorrent network",
|
||||||
|
"transformations": [],
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"text": "No DHT"
|
||||||
|
},
|
||||||
|
"-1": {
|
||||||
|
"text": "Firewalled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "dark-red",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "semi-dark-green",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 6,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 13,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "qbittorrent_dht_nodes",
|
||||||
|
"instant": true,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "DHT nodes",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "DHT nodes",
|
||||||
|
"transformations": [],
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "bytes"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 9,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "sum by (app) (qbittorrent_dl_info_data_total)",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": " ",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Data downloaded",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "bytes"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "sum by (app) (qbittorrent_up_info_data_total)",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": " ",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Data uploaded",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 15,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "(sum by (app) (qbittorrent_up_info_data_total)) / (sum by (app) (qbittorrent_dl_info_data_total))",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "{{label_name}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Accumulated ratio",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"decimals": 1,
|
||||||
|
"mappings": [],
|
||||||
|
"max": 48000000,
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "dark-red",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "dark-green",
|
||||||
|
"value": 8000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "binBps"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 18,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 16,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(qbittorrent_dl_info_data_total[2m])",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Download speed",
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"decimals": 1,
|
||||||
|
"mappings": [],
|
||||||
|
"max": 18000000,
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "dark-red",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "dark-green",
|
||||||
|
"value": 4000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "binBps"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 3,
|
||||||
|
"x": 21,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 17,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(qbittorrent_up_info_data_total[2m])",
|
||||||
|
"hide": false,
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Upload speed",
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fill": 1,
|
||||||
|
"fillGradient": 0,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 4
|
||||||
|
},
|
||||||
|
"hiddenSeries": false,
|
||||||
|
"id": 15,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"options": {
|
||||||
|
"alertThreshold": true
|
||||||
|
},
|
||||||
|
"percentage": false,
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(qbittorrent_dl_info_data_total[2m])",
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "Download",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(qbittorrent_up_info_data_total[2m])",
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "Upload",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeRegions": [],
|
||||||
|
"title": "Transfer Rates",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"mode": "time",
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "binBps",
|
||||||
|
"logBase": 1,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "binBps",
|
||||||
|
"logBase": 1,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": true,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fill": 1,
|
||||||
|
"fillGradient": 0,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 14,
|
||||||
|
"x": 0,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"hiddenSeries": false,
|
||||||
|
"id": 11,
|
||||||
|
"legend": {
|
||||||
|
"alignAsTable": true,
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"rightSide": true,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": false,
|
||||||
|
"linewidth": 0,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null as zero",
|
||||||
|
"options": {
|
||||||
|
"alertThreshold": true
|
||||||
|
},
|
||||||
|
"percentage": false,
|
||||||
|
"pluginVersion": "9.4.3",
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": true,
|
||||||
|
"steppedLine": true,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(qbittorrent_torrents_count{category=~\"${categories}\", status!=\"complete\"}) by (status)",
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "{{status}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeRegions": [],
|
||||||
|
"title": "Torrents by status",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"mode": "time",
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"logBase": 1,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"logBase": 1,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": []
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 10,
|
||||||
|
"x": 14,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"id": 12,
|
||||||
|
"links": [],
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"pieType": "pie",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "7.2.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(qbittorrent_torrents_count{category=~\"${categories}\",status!=\"complete\"}) by (category)",
|
||||||
|
"interval": "",
|
||||||
|
"legendFormat": "{{category}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Torrents by categories",
|
||||||
|
"type": "piechart"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "10s",
|
||||||
|
"revision": 1,
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": ".*",
|
||||||
|
"current": {
|
||||||
|
"selected": false,
|
||||||
|
"text": "All",
|
||||||
|
"value": "$__all"
|
||||||
|
},
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${data_source}"
|
||||||
|
},
|
||||||
|
"definition": "label_values(qbittorrent_torrents_count, category)",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": true,
|
||||||
|
"label": "Categories",
|
||||||
|
"multi": true,
|
||||||
|
"name": "categories",
|
||||||
|
"options": [],
|
||||||
|
"query": {
|
||||||
|
"query": "label_values(qbittorrent_torrents_count, category)",
|
||||||
|
"refId": "Prometheus-categories-Variable-Query"
|
||||||
|
},
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 0,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"selected": true,
|
||||||
|
"text": "Prometheus",
|
||||||
|
"value": "Prometheus"
|
||||||
|
},
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Data Source",
|
||||||
|
"multi": false,
|
||||||
|
"name": "data_source",
|
||||||
|
"options": [],
|
||||||
|
"query": "prometheus",
|
||||||
|
"queryValue": "",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "datasource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Qbittorrent",
|
||||||
|
"uid": "eKyTETFMk",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
BIN
grafana/screenshot.png
Normal file
BIN
grafana/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
284
pdm.lock
generated
Normal file
284
pdm.lock
generated
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# This file is @generated by PDM.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
groups = ["default", "dev"]
|
||||||
|
strategy = ["cross_platform"]
|
||||||
|
lock_version = "4.4"
|
||||||
|
content_hash = "sha256:474c773ee86217d652b5fc82f921ce2fbb513b244d50fbe82b74ea9e47dad96e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "23.11.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "The uncompromising code formatter."
|
||||||
|
dependencies = [
|
||||||
|
"click>=8.0.0",
|
||||||
|
"mypy-extensions>=0.4.3",
|
||||||
|
"packaging>=22.0",
|
||||||
|
"pathspec>=0.9.0",
|
||||||
|
"platformdirs>=2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
|
||||||
|
{file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
|
||||||
|
{file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
|
||||||
|
{file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
|
||||||
|
{file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
|
||||||
|
{file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Composable command line interface toolkit"
|
||||||
|
dependencies = [
|
||||||
|
"colorama; platform_system == \"Windows\"",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
summary = "Cross-platform colored terminal text."
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.3.2"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Code coverage measurement for Python"
|
||||||
|
files = [
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
|
||||||
|
{file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
|
||||||
|
{file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
|
||||||
|
{file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
|
||||||
|
{file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "brain-dead simple config-ini parsing"
|
||||||
|
files = [
|
||||||
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.12.0"
|
||||||
|
requires_python = ">=3.8.0"
|
||||||
|
summary = "A Python utility / library to sort Python imports."
|
||||||
|
files = [
|
||||||
|
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
|
||||||
|
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires_python = ">=3.5"
|
||||||
|
summary = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "pathspec"
|
||||||
|
version = "0.11.2"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
files = [
|
||||||
|
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
|
||||||
|
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
files = [
|
||||||
|
{file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"},
|
||||||
|
{file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.3.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "plugin and hook calling mechanisms for python"
|
||||||
|
files = [
|
||||||
|
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
|
||||||
|
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "pytest"
|
||||||
|
version = "7.4.3"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "pytest: simple powerful testing with Python"
|
||||||
|
dependencies = [
|
||||||
|
"colorama; sys_platform == \"win32\"",
|
||||||
|
"iniconfig",
|
||||||
|
"packaging",
|
||||||
|
"pluggy<2.0,>=0.12",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
|
||||||
|
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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"},
|
||||||
|
]
|
||||||
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[project]
|
||||||
|
name = "prometheus-qbittorrent-exporter"
|
||||||
|
version = "1.4.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.4.0.tar.gz"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
qbittorrent-exporter = "qbittorrent_exporter.exporter:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["pdm-backend"]
|
||||||
|
build-backend = "pdm.backend"
|
||||||
|
|
||||||
|
[tool.pdm.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.4.3",
|
||||||
|
"isort>=5.12.0",
|
||||||
|
"black>=23.11.0",
|
||||||
|
"coverage>=7.3.2",
|
||||||
|
]
|
||||||
@@ -1,209 +1,309 @@
|
|||||||
import time
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
from attrdict import AttrDict
|
|
||||||
from qbittorrentapi import Client, TorrentStates
|
|
||||||
from prometheus_client import start_http_server
|
|
||||||
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
|
|
||||||
import logging
|
import logging
|
||||||
from pythonjsonlogger import jsonlogger
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import StrEnum, auto
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
from prometheus_client import start_http_server
|
||||||
|
from prometheus_client.core import REGISTRY, CounterMetricFamily, GaugeMetricFamily
|
||||||
|
from pythonjsonlogger import jsonlogger
|
||||||
|
from qbittorrentapi import Client, TorrentStates
|
||||||
|
|
||||||
# Enable dumps on stderr in case of segfault
|
# Enable dumps on stderr in case of segfault
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
logger = None
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class QbittorrentMetricsCollector():
|
class MetricType(StrEnum):
|
||||||
TORRENT_STATUSES = [
|
"""
|
||||||
"downloading",
|
Represents possible metric types (used in this project).
|
||||||
"uploading",
|
"""
|
||||||
"complete",
|
|
||||||
"checking",
|
|
||||||
"errored",
|
|
||||||
"paused",
|
|
||||||
]
|
|
||||||
|
|
||||||
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.torrents = None
|
|
||||||
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"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def collect(self):
|
def collect(self) -> Iterable[GaugeMetricFamily | CounterMetricFamily]:
|
||||||
try:
|
"""
|
||||||
self.torrents = self.client.torrents.info()
|
Yields Prometheus gauges and counters from metrics collected from qbittorrent.
|
||||||
except Exception as e:
|
"""
|
||||||
logger.error(f"Couldn't get server info: {e}")
|
metrics: list[Metric] = self.get_qbittorrent_metrics()
|
||||||
|
|
||||||
metrics = 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]:
|
||||||
|
"""
|
||||||
|
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:
|
||||||
response = self.client.transfer.info
|
response = self.client.transfer.info
|
||||||
version = self.client.app.version
|
version = self.client.app.version
|
||||||
self.torrents = self.client.torrents.info()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Couldn't get server info: {e}")
|
logger.error(f"Couldn't get server info: {e}")
|
||||||
response = None
|
|
||||||
version = ""
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
Metric(
|
||||||
"name": f"{self.config['metrics_prefix']}_up",
|
name=f"{self.config['metrics_prefix']}_up",
|
||||||
"value": response is not None,
|
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)
|
||||||
|
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 categories: {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 []
|
||||||
|
|
||||||
if not self.torrents:
|
def _filter_torrents_by_category(
|
||||||
return []
|
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"] == "")
|
||||||
|
]
|
||||||
|
|
||||||
|
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": ""}
|
||||||
|
|
||||||
metrics = []
|
|
||||||
categories.Uncategorized = AttrDict({'name': 'Uncategorized', 'savePath': ''})
|
|
||||||
for category in categories:
|
for category in categories:
|
||||||
category_torrents = [t for t in self.torrents if t['category'] == category or (category == "Uncategorized" and t['category'] == "")]
|
category_torrents = self._filter_torrents_by_category(category, torrents)
|
||||||
|
for state in TorrentStates:
|
||||||
for status in self.TORRENT_STATUSES:
|
state_torrents = self._filter_torrents_by_state(
|
||||||
status_prop = f"is_{status}"
|
state, category_torrents
|
||||||
status_torrents = [
|
)
|
||||||
t for t in category_torrents if getattr(TorrentStates, status_prop).fget(TorrentStates(t['state']))
|
metric = self._construct_metric(
|
||||||
]
|
state.value, category, len(state_torrents)
|
||||||
metrics.append({
|
)
|
||||||
"name": f"{self.config['metrics_prefix']}_torrents_count",
|
metrics.append(metric)
|
||||||
"value": len(status_torrents),
|
|
||||||
"labels": {
|
|
||||||
"status": status,
|
|
||||||
"category": category,
|
|
||||||
},
|
|
||||||
"help": f"Number of torrents in status {status} under category {category}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return metrics
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
class SignalHandler():
|
class ShutdownSignalHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.shutdown = False
|
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.shutdown
|
return self.shutdown_count > 0
|
||||||
|
|
||||||
def _on_signal_received(self, signal, frame):
|
def _on_signal_received(self, signal, frame):
|
||||||
|
if self.shutdown_count > 1:
|
||||||
|
logger.warn("Forcibly killing exporter")
|
||||||
|
sys.exit(1)
|
||||||
logger.info("Exporter is shutting down")
|
logger.info("Exporter is shutting down")
|
||||||
self.shutdown = True
|
self.shutdown_count += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _get_config_value(key: str, default: str = "") -> str:
|
||||||
|
input_path = os.environ.get("FILE__" + key, None)
|
||||||
|
if input_path is not None:
|
||||||
|
try:
|
||||||
|
with open(input_path, "r") as input_file:
|
||||||
|
return input_file.read().strip()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(f"Unable to read value for {key} from {input_path}: {str(e)}")
|
||||||
|
|
||||||
|
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():
|
||||||
config = {
|
# Init logger so it can be used
|
||||||
"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"),
|
|
||||||
"metrics_prefix": os.environ.get("METRICS_PREFIX", "qbittorrent"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Register signal handler
|
|
||||||
signal_handler = SignalHandler()
|
|
||||||
|
|
||||||
# Init logger
|
|
||||||
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 = logging.getLogger()
|
|
||||||
logger.addHandler(logHandler)
|
logger.addHandler(logHandler)
|
||||||
|
logger.setLevel("INFO") # default until config is loaded
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# set level once config has been loaded
|
||||||
logger.setLevel(config["log_level"])
|
logger.setLevel(config["log_level"])
|
||||||
|
|
||||||
|
# Register signal handler
|
||||||
|
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 post 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.1.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.1.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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
276
tests/exporter_test.py
Normal file
276
tests/exporter_test.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from prometheus_client.metrics_core import CounterMetricFamily, GaugeMetricFamily
|
||||||
|
|
||||||
|
from qbittorrent_exporter.exporter import (
|
||||||
|
Metric,
|
||||||
|
MetricType,
|
||||||
|
QbittorrentMetricsCollector,
|
||||||
|
)
|
||||||
|
from qbittorrentapi import TorrentStates
|
||||||
|
|
||||||
|
|
||||||
|
class TestQbittorrentMetricsCollector(unittest.TestCase):
|
||||||
|
@patch("qbittorrent_exporter.exporter.Client")
|
||||||
|
def setUp(self, mock_client):
|
||||||
|
self.mock_client = mock_client
|
||||||
|
self.config = {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "8080",
|
||||||
|
"username": "user",
|
||||||
|
"password": "pass",
|
||||||
|
"verify_webui_certificate": False,
|
||||||
|
"metrics_prefix": "qbittorrent",
|
||||||
|
}
|
||||||
|
self.torrentsState = [
|
||||||
|
{"name": "Torrent DOWNLOADING 1", "state": TorrentStates.DOWNLOADING},
|
||||||
|
{"name": "Torrent UPLOADING 1", "state": TorrentStates.UPLOADING},
|
||||||
|
{"name": "Torrent DOWNLOADING 2", "state": TorrentStates.DOWNLOADING},
|
||||||
|
{"name": "Torrent UPLOADING 2", "state": TorrentStates.UPLOADING},
|
||||||
|
]
|
||||||
|
self.torrentsCategories = [
|
||||||
|
{"name": "Torrent Movies 1", "category": "Movies"},
|
||||||
|
{"name": "Torrent Music 1", "category": "Music"},
|
||||||
|
{"name": "Torrent Movies 2", "category": "Movies"},
|
||||||
|
{"name": "Torrent unknown", "category": ""},
|
||||||
|
{"name": "Torrent Music 2", "category": "Music"},
|
||||||
|
{"name": "Torrent Uncategorized 1", "category": "Uncategorized"},
|
||||||
|
]
|
||||||
|
self.collector = QbittorrentMetricsCollector(self.config)
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
self.assertEqual(self.collector.config, self.config)
|
||||||
|
self.mock_client.assert_called_once_with(
|
||||||
|
host=self.config["host"],
|
||||||
|
port=self.config["port"],
|
||||||
|
username=self.config["username"],
|
||||||
|
password=self.config["password"],
|
||||||
|
VERIFY_WEBUI_CERTIFICATE=self.config["verify_webui_certificate"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_collect_gauge(self):
|
||||||
|
mock_metric = Metric(
|
||||||
|
name="test_gauge",
|
||||||
|
metric_type=MetricType.GAUGE,
|
||||||
|
help_text="Test Gauge",
|
||||||
|
labels={"label1": "value1"},
|
||||||
|
value=10,
|
||||||
|
)
|
||||||
|
self.collector.get_qbittorrent_metrics = MagicMock(return_value=[mock_metric])
|
||||||
|
|
||||||
|
result = next(self.collector.collect())
|
||||||
|
|
||||||
|
self.assertIsInstance(result, GaugeMetricFamily)
|
||||||
|
self.assertEqual(result.name, "test_gauge")
|
||||||
|
self.assertEqual(result.documentation, "Test Gauge")
|
||||||
|
self.assertEqual(result.samples[0].labels, {"label1": "value1"})
|
||||||
|
self.assertEqual(result.samples[0].value, 10)
|
||||||
|
|
||||||
|
def test_collect_counter(self):
|
||||||
|
mock_metric = Metric(
|
||||||
|
name="test_counter",
|
||||||
|
metric_type=MetricType.COUNTER,
|
||||||
|
help_text="Test Counter",
|
||||||
|
labels={"label2": "value2"},
|
||||||
|
value=230,
|
||||||
|
)
|
||||||
|
self.collector.get_qbittorrent_metrics = MagicMock(return_value=[mock_metric])
|
||||||
|
|
||||||
|
result = next(self.collector.collect())
|
||||||
|
|
||||||
|
self.assertIsInstance(result, CounterMetricFamily)
|
||||||
|
self.assertEqual(result.name, "test_counter")
|
||||||
|
self.assertEqual(result.documentation, "Test Counter")
|
||||||
|
self.assertEqual(result.samples[0].labels, {"label2": "value2"})
|
||||||
|
self.assertEqual(result.samples[0].value, 230)
|
||||||
|
|
||||||
|
def test_get_qbittorrent_metrics(self):
|
||||||
|
metrics = self.collector.get_qbittorrent_metrics()
|
||||||
|
self.assertNotEqual(len(metrics), 0)
|
||||||
|
|
||||||
|
def test_fetch_categories(self):
|
||||||
|
# Mock the client.torrent_categories.categories attribute
|
||||||
|
self.collector.client.torrent_categories.categories = {
|
||||||
|
"category1": {"name": "Category 1"},
|
||||||
|
"category2": {"name": "Category 2"},
|
||||||
|
"category3": {"name": "Category 3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = self.collector._fetch_categories()
|
||||||
|
self.assertIsInstance(categories, dict)
|
||||||
|
self.assertNotEqual(len(categories), 0)
|
||||||
|
self.assertEqual(categories["category1"]["name"], "Category 1")
|
||||||
|
self.assertEqual(categories["category2"]["name"], "Category 2")
|
||||||
|
self.assertEqual(categories["category3"]["name"], "Category 3")
|
||||||
|
|
||||||
|
def test_fetch_categories_exception(self):
|
||||||
|
self.collector.client.torrent_categories.categories = Exception(
|
||||||
|
"Error fetching categories"
|
||||||
|
)
|
||||||
|
categories = self.collector._fetch_categories()
|
||||||
|
self.assertEqual(categories, {})
|
||||||
|
|
||||||
|
def test_fetch_torrents_success(self):
|
||||||
|
# Mock the return value of self.client.torrents.info()
|
||||||
|
self.collector.client.torrents.info.return_value = [
|
||||||
|
{"name": "Torrent 1", "size": 100},
|
||||||
|
{"name": "Torrent 2", "size": 200},
|
||||||
|
{"name": "Torrent 3", "size": 300},
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_result = [
|
||||||
|
{"name": "Torrent 1", "size": 100},
|
||||||
|
{"name": "Torrent 2", "size": 200},
|
||||||
|
{"name": "Torrent 3", "size": 300},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = self.collector._fetch_torrents()
|
||||||
|
self.assertEqual(result, expected_result)
|
||||||
|
|
||||||
|
def test_fetch_torrents_exception(self):
|
||||||
|
# Mock an exception being raised by self.client.torrents.info()
|
||||||
|
self.collector.client.torrents.info.side_effect = Exception("Connection error")
|
||||||
|
|
||||||
|
expected_result = []
|
||||||
|
|
||||||
|
result = self.collector._fetch_torrents()
|
||||||
|
self.assertEqual(result, expected_result)
|
||||||
|
|
||||||
|
def test_filter_torrents_by_state(self):
|
||||||
|
expected = [
|
||||||
|
{"name": "Torrent DOWNLOADING 1", "state": TorrentStates.DOWNLOADING},
|
||||||
|
{"name": "Torrent DOWNLOADING 2", "state": TorrentStates.DOWNLOADING},
|
||||||
|
]
|
||||||
|
result = self.collector._filter_torrents_by_state(
|
||||||
|
TorrentStates.DOWNLOADING, self.torrentsState
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
{"name": "Torrent UPLOADING 1", "state": TorrentStates.UPLOADING},
|
||||||
|
{"name": "Torrent UPLOADING 2", "state": TorrentStates.UPLOADING},
|
||||||
|
]
|
||||||
|
result = self.collector._filter_torrents_by_state(
|
||||||
|
TorrentStates.UPLOADING, self.torrentsState
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
expected = []
|
||||||
|
result = self.collector._filter_torrents_by_state(
|
||||||
|
TorrentStates.ERROR, self.torrentsState
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_filter_torrents_by_category(self):
|
||||||
|
expected_result = [
|
||||||
|
{"name": "Torrent Movies 1", "category": "Movies"},
|
||||||
|
{"name": "Torrent Movies 2", "category": "Movies"},
|
||||||
|
]
|
||||||
|
result = self.collector._filter_torrents_by_category(
|
||||||
|
"Movies", self.torrentsCategories
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected_result)
|
||||||
|
|
||||||
|
expected_result = [
|
||||||
|
{"name": "Torrent unknown", "category": ""},
|
||||||
|
{"name": "Torrent Uncategorized 1", "category": "Uncategorized"},
|
||||||
|
]
|
||||||
|
result = self.collector._filter_torrents_by_category(
|
||||||
|
"Uncategorized", self.torrentsCategories
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected_result)
|
||||||
|
|
||||||
|
expected_result = []
|
||||||
|
result = self.collector._filter_torrents_by_category(
|
||||||
|
"Books", self.torrentsCategories
|
||||||
|
)
|
||||||
|
self.assertEqual(result, expected_result)
|
||||||
|
|
||||||
|
def test_construct_metric_with_valid_state_and_category(self):
|
||||||
|
state = "downloading"
|
||||||
|
category = "movies"
|
||||||
|
count = 10
|
||||||
|
|
||||||
|
metric = self.collector._construct_metric(state, category, count)
|
||||||
|
|
||||||
|
self.assertEqual(metric.name, "qbittorrent_torrents_count")
|
||||||
|
self.assertEqual(metric.value, count)
|
||||||
|
self.assertEqual(metric.labels["status"], state)
|
||||||
|
self.assertEqual(metric.labels["category"], category)
|
||||||
|
self.assertEqual(
|
||||||
|
metric.help_text,
|
||||||
|
f"Number of torrents in status {state} under category {category}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_construct_metric_with_empty_state_and_category(self):
|
||||||
|
state = ""
|
||||||
|
category = ""
|
||||||
|
count = 5
|
||||||
|
|
||||||
|
metric = self.collector._construct_metric(state, category, count)
|
||||||
|
|
||||||
|
self.assertEqual(metric.name, "qbittorrent_torrents_count")
|
||||||
|
self.assertEqual(metric.value, count)
|
||||||
|
self.assertEqual(metric.labels["status"], state)
|
||||||
|
self.assertEqual(metric.labels["category"], category)
|
||||||
|
self.assertEqual(
|
||||||
|
metric.help_text, "Number of torrents in status under category "
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_qbittorrent_status_metrics(self):
|
||||||
|
self.collector.client.transfer.info = {"connection_status": "connected"}
|
||||||
|
self.collector.client.app.version = "1.2.3"
|
||||||
|
|
||||||
|
expected_metrics = [
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_up",
|
||||||
|
value=True,
|
||||||
|
labels={"version": "1.2.3"},
|
||||||
|
help_text=(
|
||||||
|
"Whether the qBittorrent server is answering requests from this"
|
||||||
|
" exporter. A `version` label with the server version is added."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_connected",
|
||||||
|
value=True,
|
||||||
|
labels={},
|
||||||
|
help_text=(
|
||||||
|
"Whether the qBittorrent server is connected to the Bittorrent"
|
||||||
|
" network."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_firewalled",
|
||||||
|
value=False,
|
||||||
|
labels={},
|
||||||
|
help_text=(
|
||||||
|
"Whether the qBittorrent server is connected to the Bittorrent"
|
||||||
|
" network but is behind a firewall."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_dht_nodes",
|
||||||
|
value=0,
|
||||||
|
labels={},
|
||||||
|
help_text="Number of DHT nodes connected to.",
|
||||||
|
),
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_dl_info_data",
|
||||||
|
value=0,
|
||||||
|
labels={},
|
||||||
|
help_text="Data downloaded since the server started, in bytes.",
|
||||||
|
metric_type=MetricType.COUNTER,
|
||||||
|
),
|
||||||
|
Metric(
|
||||||
|
name="qbittorrent_up_info_data",
|
||||||
|
value=0,
|
||||||
|
labels={},
|
||||||
|
help_text="Data uploaded since the server started, in bytes.",
|
||||||
|
metric_type=MetricType.COUNTER,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
metrics = self.collector._get_qbittorrent_status_metrics()
|
||||||
|
self.assertEqual(metrics, expected_metrics)
|
||||||
13
tests/metric_test.py
Normal file
13
tests/metric_test.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from qbittorrent_exporter.exporter import Metric, MetricType
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetric(unittest.TestCase):
|
||||||
|
def test_metric_initialization(self):
|
||||||
|
metric = Metric(name="test_metric", value=10)
|
||||||
|
self.assertEqual(metric.name, "test_metric")
|
||||||
|
self.assertEqual(metric.value, 10)
|
||||||
|
self.assertEqual(metric.labels, {})
|
||||||
|
self.assertEqual(metric.help_text, "")
|
||||||
|
self.assertEqual(metric.metric_type, MetricType.GAUGE)
|
||||||
Reference in New Issue
Block a user