nixos-config/modules/qbittorrent/exporter/qbittorrent_exporter.go

210 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// SPDX-FileCopyrightText: 2022-2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
package main
import (
"context"
"encoding/json"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"strconv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/exporter-toolkit/web"
)
type ApiClient struct {
apiSocket string
}
type ApiError struct {
ReturnCode int
}
type TorrentInfo struct {
Auto bool `json:"auto_tmm"`
DownloadLimit int `json:"dl_limit"`
Downloaded int `json:"completed"`
Hash string `json:"hash"`
LeechsConnected int `json:"num_leechs"`
LeechsSwarm int `json:"num_incomplete"`
Name string `json:"name"`
SeedsConnected int `json:"num_seeds"`
SeedsSwarm int `json:"num_complete"`
Size int `json:"size"`
TotalSize int `json:"total_size"`
UploadLimit int `json:"ul_limit"`
Uploaded int `json:"uploaded"`
}
type Exporter struct {
Api ApiClient
}
var (
qbittorrentTorrentDownloaded = prometheus.NewDesc(
"qbittorrent_torrent_downloaded_bytes_total",
"Amount of data downloaded",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentLeechsConnected = prometheus.NewDesc(
"qbittorrent_torrent_leechs_connected",
"Number of leechs connected to",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentLeechsSwarm = prometheus.NewDesc(
"qbittorrent_torrent_leechs_swarm",
"Number of leechs in the swarm",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentSeedsConnected = prometheus.NewDesc(
"qbittorrent_torrent_seeds_connected",
"Number of seeds connected to",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentSeedsSwarm = prometheus.NewDesc(
"qbittorrent_torrent_seeds_swarm",
"Number of seeds in the swarm",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentSize = prometheus.NewDesc(
"qbittorrent_torrent_size_bytes_total",
"Size of selected torrent data",
[]string{"hash", "name"}, nil,
)
qbittorrentTorrentUploaded = prometheus.NewDesc(
"qbittorrent_torrent_uploaded_bytes_total",
"Amount of data uploaded",
[]string{"hash", "name"}, nil,
)
)
func (e ApiError) Error() string {
return strconv.Itoa(e.ReturnCode)
}
func CreateApiClient(apiSocket string) (c ApiClient) {
return ApiClient{
apiSocket: apiSocket,
}
}
func (c ApiClient) doRequest(group string, method string, parameters url.Values) (body []byte, err error) {
destinationUrl, err := url.Parse("http://unix/api/v2/" + group + "/" + method)
if err != nil {
log.Println(err)
return []byte{}, err
}
req := http.Request{
Method: "GET",
URL: destinationUrl,
Header: http.Header{},
Form: parameters,
}
httpClient := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", c.apiSocket)
},
},
}
res, err := httpClient.Do(&req)
if err != nil {
log.Println(err)
return []byte{}, err
}
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return []byte{}, err
}
body = bodyBytes
if res.StatusCode != 200 {
err = ApiError{ReturnCode: res.StatusCode}
}
return
}
func (c ApiClient) TorrentsInfo() (torrentsInfo []TorrentInfo, err error) {
torrentsInfo = []TorrentInfo{}
body, err := c.doRequest("torrents", "info", url.Values{})
if err != nil {
return []TorrentInfo{}, err
}
json.Unmarshal(body, &torrentsInfo)
return
}
func (e Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- qbittorrentTorrentDownloaded
ch <- qbittorrentTorrentLeechsConnected
ch <- qbittorrentTorrentLeechsSwarm
ch <- qbittorrentTorrentSeedsConnected
ch <- qbittorrentTorrentSeedsSwarm
ch <- qbittorrentTorrentSize
ch <- qbittorrentTorrentUploaded
}
func (e Exporter) Collect(ch chan<- prometheus.Metric) {
torrentsInfo, err := e.Api.TorrentsInfo()
if err != nil {
log.Println(err)
return
}
for _, torrentInfo := range torrentsInfo {
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentDownloaded, prometheus.CounterValue, float64(torrentInfo.Downloaded), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentLeechsConnected, prometheus.GaugeValue, float64(torrentInfo.LeechsConnected), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentLeechsSwarm, prometheus.GaugeValue, float64(torrentInfo.LeechsSwarm), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentSeedsConnected, prometheus.GaugeValue, float64(torrentInfo.SeedsConnected), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentSeedsSwarm, prometheus.GaugeValue, float64(torrentInfo.SeedsSwarm), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentSize, prometheus.GaugeValue, float64(torrentInfo.Size), torrentInfo.Hash, torrentInfo.Name)
ch <- prometheus.MustNewConstMetric(qbittorrentTorrentUploaded, prometheus.CounterValue, float64(torrentInfo.Uploaded), torrentInfo.Hash, torrentInfo.Name)
}
}
func main() {
e := Exporter{
Api: CreateApiClient(
os.Getenv("QBITTORRENT_API_SOCKET"),
),
}
prometheus.MustRegister(e)
http.Handle("/metrics", promhttp.Handler())
landingPage, err := web.NewLandingPage(web.LandingConfig{
Name: "qBittorrent Exporter",
Links: []web.LandingLinks{
{
Address: "/metrics",
Text: "Metrics",
},
{
Address: "https://git.sbruder.de/simon/nixos-config/src/branch/master/modules/qbittorrent/exporter",
Text: "Source Code",
},
{
Address: "https://www.gnu.org/licenses/agpl-3.0.txt",
Text: "Released under the AGPLv3 or later",
},
},
})
if err != nil {
log.Fatalf("Failed to create landing page: %v", err)
}
http.Handle("/", landingPage)
listenAddress := os.Getenv("QBITTORRENT_EXPORTER_LISTEN_ADDRESS")
if listenAddress == "" {
listenAddress = ":9561" // this reuses the port number of fru1tstands exporter
}
log.Printf("Starting HTTP server at at %s", listenAddress)
log.Fatalf("Failed to start http server: %v", http.ListenAndServe(listenAddress, nil))
}