blumeops/containers/transmission-exporter/exporter.py

164 lines
5.1 KiB
Python
Raw Normal View History

Replace transmission-exporter with homegrown Python exporter (#283) ## Summary - Replace unmaintained `metalmatze/transmission-exporter:master` sidecar with a homegrown Python exporter - Uses `prometheus_client` + `transmission-rpc` with collect-on-scrape pattern (fresh metrics per scrape, no stale labels) - Same metric names so existing Grafana Transmission dashboard works unchanged - Container built with `uv` for dependency management, follows `grafana-sidecar` Dockerfile pattern ## Changes - **New:** `containers/transmission-exporter/exporter.py` — single-file exporter (~130 lines) - **New:** `containers/transmission-exporter/Dockerfile` — multi-stage Alpine build with uv - **Modified:** `argocd/manifests/torrent/deployment.yaml` — swap sidecar image reference - **Modified:** `argocd/manifests/torrent/kustomization.yaml` — add image tag entry - **Modified:** `service-versions.yaml` — add transmission-exporter entry ## Deployment and Testing - [ ] Build container: `mise run container-build-and-release transmission-exporter` - [ ] Update kustomization.yaml newTag with build SHA - [ ] Branch deploy: `argocd app set torrent --revision feature/transmission-exporter-python && argocd app sync torrent` - [ ] Verify metrics: `kubectl -n torrent --context=minikube-indri port-forward svc/transmission 19091:19091` then `curl localhost:19091/metrics | grep transmission_` - [ ] Verify Grafana Transmission dashboard panels populate - [ ] After merge: `argocd app set torrent --revision main && argocd app sync torrent` Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/283
2026-03-04 21:55:00 -08:00
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "prometheus-client",
# "transmission-rpc",
# ]
# ///
"""Minimal Prometheus exporter for Transmission, using collect-on-scrape."""
import os
import sys
import urllib.parse
from wsgiref.simple_server import make_server
from prometheus_client import make_wsgi_app
from prometheus_client.core import REGISTRY, GaugeMetricFamily
from transmission_rpc import Client
def parse_addr(addr: str) -> dict:
"""Parse TRANSMISSION_ADDR into kwargs for transmission_rpc.Client."""
parsed = urllib.parse.urlparse(addr)
kwargs: dict = {}
if parsed.hostname:
kwargs["host"] = parsed.hostname
if parsed.port:
kwargs["port"] = parsed.port
if parsed.scheme == "https":
kwargs["protocol"] = "https"
if parsed.path and parsed.path != "/":
kwargs["path"] = parsed.path.strip("/")
if parsed.username:
kwargs["username"] = parsed.username
if parsed.password:
kwargs["password"] = parsed.password
return kwargs
class TransmissionCollector:
def __init__(self, client_kwargs: dict):
self._client_kwargs = client_kwargs
def collect(self):
try:
client = Client(**self._client_kwargs)
session = client.session_stats()
torrents = client.get_torrents()
except Exception as e:
print(f"Error collecting metrics: {e}", file=sys.stderr)
return
yield _gauge(
"transmission_session_stats_download_speed_bytes",
"Current download speed in bytes/s",
session.download_speed,
)
yield _gauge(
"transmission_session_stats_upload_speed_bytes",
"Current upload speed in bytes/s",
session.upload_speed,
)
yield _gauge(
"transmission_session_stats_torrents_active",
"Number of active torrents",
session.active_torrent_count,
)
yield _gauge(
"transmission_session_stats_torrents_total",
"Total number of torrents",
session.torrent_count,
)
downloaded = GaugeMetricFamily(
"transmission_session_stats_downloaded_bytes",
"Total bytes downloaded",
labels=["type"],
)
downloaded.add_metric(["cumulative"], session.cumulative_stats.downloaded_bytes)
yield downloaded
uploaded = GaugeMetricFamily(
"transmission_session_stats_uploaded_bytes",
"Total bytes uploaded",
labels=["type"],
)
uploaded.add_metric(["cumulative"], session.cumulative_stats.uploaded_bytes)
yield uploaded
t_download = GaugeMetricFamily(
"transmission_torrent_download_bytes",
"Torrent total downloaded bytes",
labels=["name"],
)
t_upload = GaugeMetricFamily(
"transmission_torrent_upload_bytes",
"Torrent total uploaded bytes",
labels=["name"],
)
t_ratio = GaugeMetricFamily(
"transmission_torrent_ratio",
"Torrent upload ratio",
labels=["name"],
)
t_uploaded_ever = GaugeMetricFamily(
"transmission_torrent_uploaded_ever",
"Torrent total uploaded ever in bytes",
labels=["name"],
)
t_download_rate = GaugeMetricFamily(
"transmission_torrent_download_rate_bytes",
"Torrent current download rate in bytes/s",
labels=["name"],
)
t_upload_rate = GaugeMetricFamily(
"transmission_torrent_upload_rate_bytes",
"Torrent current upload rate in bytes/s",
labels=["name"],
)
Replace transmission-exporter with homegrown Python exporter (#283) ## Summary - Replace unmaintained `metalmatze/transmission-exporter:master` sidecar with a homegrown Python exporter - Uses `prometheus_client` + `transmission-rpc` with collect-on-scrape pattern (fresh metrics per scrape, no stale labels) - Same metric names so existing Grafana Transmission dashboard works unchanged - Container built with `uv` for dependency management, follows `grafana-sidecar` Dockerfile pattern ## Changes - **New:** `containers/transmission-exporter/exporter.py` — single-file exporter (~130 lines) - **New:** `containers/transmission-exporter/Dockerfile` — multi-stage Alpine build with uv - **Modified:** `argocd/manifests/torrent/deployment.yaml` — swap sidecar image reference - **Modified:** `argocd/manifests/torrent/kustomization.yaml` — add image tag entry - **Modified:** `service-versions.yaml` — add transmission-exporter entry ## Deployment and Testing - [ ] Build container: `mise run container-build-and-release transmission-exporter` - [ ] Update kustomization.yaml newTag with build SHA - [ ] Branch deploy: `argocd app set torrent --revision feature/transmission-exporter-python && argocd app sync torrent` - [ ] Verify metrics: `kubectl -n torrent --context=minikube-indri port-forward svc/transmission 19091:19091` then `curl localhost:19091/metrics | grep transmission_` - [ ] Verify Grafana Transmission dashboard panels populate - [ ] After merge: `argocd app set torrent --revision main && argocd app sync torrent` Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/283
2026-03-04 21:55:00 -08:00
t_done = GaugeMetricFamily(
"transmission_torrent_done",
"Torrent percent done (0.0-1.0)",
labels=["name"],
)
for t in torrents:
name = t.name or "unknown"
t_download.add_metric([name], t.total_size * t.percent_done)
t_upload.add_metric([name], t.uploaded_ever)
t_download_rate.add_metric([name], t.rate_download)
t_upload_rate.add_metric([name], t.rate_upload)
Replace transmission-exporter with homegrown Python exporter (#283) ## Summary - Replace unmaintained `metalmatze/transmission-exporter:master` sidecar with a homegrown Python exporter - Uses `prometheus_client` + `transmission-rpc` with collect-on-scrape pattern (fresh metrics per scrape, no stale labels) - Same metric names so existing Grafana Transmission dashboard works unchanged - Container built with `uv` for dependency management, follows `grafana-sidecar` Dockerfile pattern ## Changes - **New:** `containers/transmission-exporter/exporter.py` — single-file exporter (~130 lines) - **New:** `containers/transmission-exporter/Dockerfile` — multi-stage Alpine build with uv - **Modified:** `argocd/manifests/torrent/deployment.yaml` — swap sidecar image reference - **Modified:** `argocd/manifests/torrent/kustomization.yaml` — add image tag entry - **Modified:** `service-versions.yaml` — add transmission-exporter entry ## Deployment and Testing - [ ] Build container: `mise run container-build-and-release transmission-exporter` - [ ] Update kustomization.yaml newTag with build SHA - [ ] Branch deploy: `argocd app set torrent --revision feature/transmission-exporter-python && argocd app sync torrent` - [ ] Verify metrics: `kubectl -n torrent --context=minikube-indri port-forward svc/transmission 19091:19091` then `curl localhost:19091/metrics | grep transmission_` - [ ] Verify Grafana Transmission dashboard panels populate - [ ] After merge: `argocd app set torrent --revision main && argocd app sync torrent` Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/283
2026-03-04 21:55:00 -08:00
t_ratio.add_metric([name], t.ratio)
t_uploaded_ever.add_metric([name], t.uploaded_ever)
t_done.add_metric([name], t.percent_done)
yield t_download
yield t_upload
yield t_download_rate
yield t_upload_rate
Replace transmission-exporter with homegrown Python exporter (#283) ## Summary - Replace unmaintained `metalmatze/transmission-exporter:master` sidecar with a homegrown Python exporter - Uses `prometheus_client` + `transmission-rpc` with collect-on-scrape pattern (fresh metrics per scrape, no stale labels) - Same metric names so existing Grafana Transmission dashboard works unchanged - Container built with `uv` for dependency management, follows `grafana-sidecar` Dockerfile pattern ## Changes - **New:** `containers/transmission-exporter/exporter.py` — single-file exporter (~130 lines) - **New:** `containers/transmission-exporter/Dockerfile` — multi-stage Alpine build with uv - **Modified:** `argocd/manifests/torrent/deployment.yaml` — swap sidecar image reference - **Modified:** `argocd/manifests/torrent/kustomization.yaml` — add image tag entry - **Modified:** `service-versions.yaml` — add transmission-exporter entry ## Deployment and Testing - [ ] Build container: `mise run container-build-and-release transmission-exporter` - [ ] Update kustomization.yaml newTag with build SHA - [ ] Branch deploy: `argocd app set torrent --revision feature/transmission-exporter-python && argocd app sync torrent` - [ ] Verify metrics: `kubectl -n torrent --context=minikube-indri port-forward svc/transmission 19091:19091` then `curl localhost:19091/metrics | grep transmission_` - [ ] Verify Grafana Transmission dashboard panels populate - [ ] After merge: `argocd app set torrent --revision main && argocd app sync torrent` Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/283
2026-03-04 21:55:00 -08:00
yield t_ratio
yield t_uploaded_ever
yield t_done
def _gauge(name: str, doc: str, value: float) -> GaugeMetricFamily:
g = GaugeMetricFamily(name, doc)
g.add_metric([], value)
return g
def main():
addr = os.environ.get("TRANSMISSION_ADDR", "http://localhost:9091")
port = int(os.environ.get("EXPORTER_PORT", "19091"))
client_kwargs = parse_addr(addr)
REGISTRY.register(TransmissionCollector(client_kwargs))
print(f"Listening on :{port}, scraping {addr}")
httpd = make_server("", port, make_wsgi_app())
httpd.serve_forever()
if __name__ == "__main__":
main()