Simon Bruder
f945341668
This applies the REUSE specification to the repository, so the licensing information can be tracked for every file individually.
187 lines
5.5 KiB
Go
187 lines
5.5 KiB
Go
// SPDX-FileCopyrightText: 2022 Simon Bruder <simon@sbruder.de>
|
||
//
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"io/ioutil"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"strconv"
|
||
|
||
"github.com/prometheus/client_golang/prometheus"
|
||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||
)
|
||
|
||
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 := ioutil.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())
|
||
|
||
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))
|
||
}
|