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

210 lines
6.1 KiB
Go
Raw Normal View History

// SPDX-FileCopyrightText: 2022-2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
2022-03-18 22:14:09 +01:00
package main
import (
"context"
"encoding/json"
"io"
2022-03-18 22:14:09 +01:00
"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"
2022-03-18 22:14:09 +01:00
)
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)
2022-03-18 22:14:09 +01:00
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)
2022-03-18 22:14:09 +01:00
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))
}