summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2024-11-29 09:05:42 +0100
committerPatrick Spek <p.spek@tyil.nl>2024-11-29 09:05:42 +0100
commit7ea06abc94c4cc91e531de3c81cf7bd7b6830578 (patch)
treebb44424aed461394c95865629a1075abe5d6e481
downloadtransmission-master.tar.gz
transmission-master.tar.bz2
Initial commitHEADv0.1.0master
-rw-r--r--CHANGELOG.md10
-rw-r--r--Containerfile13
-rw-r--r--go.mod21
-rw-r--r--go.sum28
-rw-r--r--main.go313
5 files changed, 385 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4d3617d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.1.0] - 2024-11-27
+
+Initial release
diff --git a/Containerfile b/Containerfile
new file mode 100644
index 0000000..ab739df
--- /dev/null
+++ b/Containerfile
@@ -0,0 +1,13 @@
+FROM alpine:edge AS builder
+
+RUN apk add --no-cache go
+
+COPY . /usr/src
+WORKDIR /usr/src
+RUN go build
+
+FROM alpine:latest
+
+COPY --from=builder /usr/src/main /usr/local/bin/exporter
+
+CMD ["/usr/local/bin/exporter"]
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..88e8f23
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,21 @@
+module main
+
+go 1.23.2
+
+require (
+ git.tyil.nl/go/flags_list.git v0.0.0-20241128131002-f63b54d9a316
+ git.tyil.nl/go/transmission.git v0.1.0
+ github.com/prometheus/client_golang v1.20.5
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ golang.org/x/sys v0.22.0 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..63236dc
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,28 @@
+git.tyil.nl/go/flags_list.git v0.0.0-20241128131002-f63b54d9a316 h1:blfE82Ju8apYh48jFnTz+aNfaNc6QX8Ks/lRh+5ZtTo=
+git.tyil.nl/go/flags_list.git v0.0.0-20241128131002-f63b54d9a316/go.mod h1:6wQZDbWAlM6ff+IabJvU7Bo16E/s+sjIug9kBtUt0dw=
+git.tyil.nl/go/transmission.git v0.1.0 h1:AXHq7OttlPDuuJzOPhw7DgiYXzFaE8hYdbtAjEF7eqE=
+git.tyil.nl/go/transmission.git v0.1.0/go.mod h1:bV71iNodDbyE1A9Q9XoAkyEVLQ/LWXzFKSV/Wdb/WiQ=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..d01f2e3
--- /dev/null
+++ b/main.go
@@ -0,0 +1,313 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log/slog"
+ "maps"
+ "net/http"
+ "strconv"
+ "time"
+
+ "git.tyil.nl/go/flags_list.git"
+ "git.tyil.nl/go/transmission.git"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+var (
+ // Generic exporter configuration
+ httpHost string
+ httpPort int
+ interval int
+ namespace string
+ subsystem string
+ customLabels flags_list.MapVar
+
+ // Exporter-specific configuration
+ client transmission.Client
+ transmissionHost string
+ transmissionPort int
+ transmissionUsername string
+ transmissionPassword string
+
+ // Metrics
+ metricNetworkDownload *prometheus.GaugeVec
+ metricNetworkUpload *prometheus.GaugeVec
+ metricCountTorrents *prometheus.GaugeVec
+ metricCountTorrentsTotal *prometheus.GaugeVec
+ metricTorrents *prometheus.GaugeVec
+ metricFilesAdded *prometheus.GaugeVec
+ metricTrafficDownload *prometheus.GaugeVec
+ metricTrafficUpload *prometheus.GaugeVec
+ metricCountSessions *prometheus.GaugeVec
+ metricActiveSeconds *prometheus.GaugeVec
+)
+
+func main() {
+ slog.SetLogLoggerLevel(slog.LevelDebug)
+
+ // Add options for generic exporter configuration
+ flag.StringVar(&httpHost, "host", "[::]", "The host or IP address to bind the exporter to")
+ flag.IntVar(&httpPort, "port", 9091, "The port to bind the exporter to")
+ flag.IntVar(&interval, "interval", 10, "The interval at which to update the exporter metrics")
+ flag.StringVar(&namespace, "namespace", "transmission", "Namespace to use for the Prometheus metrics")
+ flag.StringVar(&subsystem, "subsystem", "", "Subsystem to use for the Prometheus metrics")
+ flag.Var(&customLabels, "label", "Additional labels to apply to the metrics")
+
+ // Add exporter-specific options
+ flag.StringVar(&transmissionHost, "transmission-host", "localhost", "The hostname for connecting to Transmission")
+ flag.IntVar(&transmissionPort, "transmission-port", 9091, "The port for connecting to Transmission")
+ flag.StringVar(&transmissionUsername, "username", "", "The username to authenticate with Transmission")
+ flag.StringVar(&transmissionPassword, "password", "", "The password to authenticate with Transmission")
+
+ // Parse options
+ flag.Parse()
+
+ // Instantiate Prometheus metrics
+ metricCountTorrents = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "count",
+ Help: "Overal state of torrents",
+ },
+ []string{"state"},
+ )
+
+ metricCountTorrentsTotal = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "count_total",
+ Help: "Total count of all torrents",
+ },
+ []string{},
+ )
+
+ metricNetworkDownload = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "network_download",
+ Help: "Current download speed of Transmission",
+ },
+ []string{},
+ )
+
+ metricNetworkUpload = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "network_upload",
+ Help: "Current upload speed of Transmission",
+ },
+ []string{},
+ )
+
+ metricFilesAdded = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "files_added",
+ Help: "Number of files added to Transmission",
+ },
+ []string{"session"},
+ )
+
+ metricTrafficDownload = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "traffic_download",
+ Help: "Total download traffic in bytes",
+ },
+ []string{"session"},
+ )
+
+ metricTrafficUpload = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "traffic_upload",
+ Help: "Total upload traffic in bytes",
+ },
+ []string{"session"},
+ )
+
+ metricCountSessions = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "count_sessions",
+ Help: "Current number of connections to Transmission",
+ },
+ []string{"session"},
+ )
+
+ metricActiveSeconds = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "active_seconds",
+ Help: "Total number of seconds Transmission has been active",
+ },
+ []string{"session"},
+ )
+
+ metricTorrents = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: namespace,
+ Subsystem: subsystem,
+ Name: "torrents",
+ Help: "Torrent status information",
+ },
+ []string{"status"},
+ )
+
+ prometheus.MustRegister(
+ metricNetworkDownload,
+ metricNetworkUpload,
+ metricCountTorrents,
+ metricCountTorrentsTotal,
+ metricTorrents,
+ metricFilesAdded,
+ metricTrafficDownload,
+ metricTrafficUpload,
+ metricCountSessions,
+ metricActiveSeconds,
+ )
+
+ // Configure Transmission client
+ client = transmission.Client{
+ Hostname: transmissionHost,
+ Port: transmissionPort,
+ Username: transmissionUsername,
+ Password: transmissionPassword,
+ }
+
+ // Run initial update to ensure the exporter doesnt publish empty values
+ update()
+
+ // Start the update loop in the background
+ go updateLoop()
+
+ // Bind HTTP handlers
+ slog.Debug("Binding HTTP handlers")
+ http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "OK\n")
+ })
+ http.Handle("/metrics", promhttp.Handler())
+
+ // Run HTTP service
+ slog.Info("Serving HTTP requests", "address", httpHost, "port", httpPort)
+ err := http.ListenAndServe(httpHost+":"+strconv.Itoa(httpPort), nil)
+
+ slog.Error(err.Error())
+}
+
+func mergeLabels(extra map[string]string) prometheus.Labels {
+ labels := make(map[string]string)
+
+ maps.Copy(labels, customLabels)
+ maps.Copy(labels, extra)
+
+ return labels
+}
+
+func update() {
+ go updateTorrents()
+ go updateStats()
+}
+
+func updateTorrents() {
+ // Instantiate list of all possible torrent statuses, with their counters set to 0
+ torrents := make(map[string]int)
+
+ for _, v := range transmission.TorrentStatus {
+ torrents[v] = 0
+ }
+
+ // Call Transmission API
+ response, err := client.CallTorrentGetAll(transmission.CallTorrentGetAllArguments{
+ Fields: []string{
+ "status",
+ },
+ })
+ if err != nil {
+ return
+ }
+
+ // Count status for all torrents
+ for _, torrent := range response.Arguments.Torrents {
+ torrents[transmission.TorrentStatus[torrent.Status]]++
+ }
+
+ // Update Prometheus metrics
+ for status, count := range torrents {
+ metricTorrents.With(mergeLabels(map[string]string{
+ "status": status,
+ })).Set(float64(count))
+ }
+}
+
+func updateStats() {
+ response, err := client.CallSessionStats()
+
+ if err != nil {
+ // ANGERY
+ }
+
+ // Network traffic
+ metricNetworkUpload.With(mergeLabels(map[string]string{})).Set(float64(response.Arguments.UploadSpeed))
+ metricNetworkDownload.With(mergeLabels(map[string]string{})).Set(float64(response.Arguments.DownloadSpeed))
+
+ // Torrent counts
+ metricCountTorrents.With(mergeLabels(map[string]string{
+ "state": "active",
+ })).Set(float64(response.Arguments.ActiveTorrentCount))
+ metricCountTorrents.With(mergeLabels(map[string]string{
+ "state": "paused",
+ })).Set(float64(response.Arguments.PausedTorrentCount))
+ metricCountTorrentsTotal.With(mergeLabels(map[string]string{})).Set(float64(response.Arguments.TorrentCount))
+
+ // Current session stats
+ metricTrafficUpload.With(mergeLabels(map[string]string{
+ "session": "current",
+ })).Set(float64(response.Arguments.CurrentStats.UploadedBytes))
+ metricTrafficDownload.With(mergeLabels(map[string]string{
+ "session": "current",
+ })).Set(float64(response.Arguments.CurrentStats.DownloadedBytes))
+ metricFilesAdded.With(mergeLabels(map[string]string{
+ "session": "current",
+ })).Set(float64(response.Arguments.CurrentStats.FilesAdded))
+ metricCountSessions.With(mergeLabels(map[string]string{
+ "session": "current",
+ })).Set(float64(response.Arguments.CurrentStats.SessionCount))
+ metricActiveSeconds.With(mergeLabels(map[string]string{
+ "session": "current",
+ })).Set(float64(response.Arguments.CurrentStats.SecondsActive))
+
+ // Cumulative session stats
+ metricTrafficUpload.With(mergeLabels(map[string]string{
+ "session": "cumulative",
+ })).Set(float64(response.Arguments.CumulativeStats.UploadedBytes))
+ metricTrafficDownload.With(mergeLabels(map[string]string{
+ "session": "cumulative",
+ })).Set(float64(response.Arguments.CumulativeStats.DownloadedBytes))
+ metricFilesAdded.With(mergeLabels(map[string]string{
+ "session": "cumulative",
+ })).Set(float64(response.Arguments.CumulativeStats.FilesAdded))
+ metricCountSessions.With(mergeLabels(map[string]string{
+ "session": "cumulative",
+ })).Set(float64(response.Arguments.CumulativeStats.SessionCount))
+ metricActiveSeconds.With(mergeLabels(map[string]string{
+ "session": "cumulative",
+ })).Set(float64(response.Arguments.CumulativeStats.SecondsActive))
+}
+
+func updateLoop() {
+ for range time.Tick(time.Duration(interval) * time.Second) {
+ update()
+ }
+}