2024-01-13 15:38:25 +01:00
|
|
|
|
// SPDX-FileCopyrightText: 2022-2024 Simon Bruder <simon@sbruder.de>
|
2024-01-06 01:19:35 +01:00
|
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
2022-03-18 22:14:09 +01:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
2024-01-13 15:38:25 +01:00
|
|
|
|
"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"
|
2024-01-13 16:00:54 +01:00
|
|
|
|
"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
|
|
|
|
|
}
|
2024-01-13 15:38:25 +01:00
|
|
|
|
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())
|
|
|
|
|
|
2024-01-13 16:00:54 +01:00
|
|
|
|
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 fru1tstand’s exporter
|
|
|
|
|
}
|
|
|
|
|
log.Printf("Starting HTTP server at at %s", listenAddress)
|
|
|
|
|
|
|
|
|
|
log.Fatalf("Failed to start http server: %v", http.ListenAndServe(listenAddress, nil))
|
|
|
|
|
}
|