diff options
| author | Patrick Spek <p.spek@tyil.nl> | 2024-11-29 09:05:42 +0100 |
|---|---|---|
| committer | Patrick Spek <p.spek@tyil.nl> | 2024-11-29 09:05:42 +0100 |
| commit | 7ea06abc94c4cc91e531de3c81cf7bd7b6830578 (patch) | |
| tree | bb44424aed461394c95865629a1075abe5d6e481 | |
| download | transmission-master.tar.gz transmission-master.tar.bz2 | |
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | Containerfile | 13 | ||||
| -rw-r--r-- | go.mod | 21 | ||||
| -rw-r--r-- | go.sum | 28 | ||||
| -rw-r--r-- | main.go | 313 |
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"] @@ -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 +) @@ -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= @@ -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() + } +} |
