├── Dockerfile ├── go.mod ├── .travis.yml ├── rtorrentexporter_test.go ├── LICENSE.md ├── README.md ├── rtorrentexporter.go ├── cmd └── rtorrent_exporter │ └── main.go ├── downloadscollector_test.go ├── go.sum └── downloadscollector.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | 3 | ENV GO111MODULE=on 4 | 5 | EXPOSE 9135 6 | 7 | WORKDIR /go/src/app 8 | COPY . . 9 | 10 | RUN go install -v ./... 11 | 12 | ENTRYPOINT ["rtorrent_exporter"] 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/rtorrent_exporter 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/mdlayher/rtorrent v0.0.0-20191011143022-58e7d496034e 7 | github.com/prometheus/client_golang v1.1.0 8 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect 9 | github.com/prometheus/common v0.7.0 // indirect 10 | github.com/prometheus/procfs v0.0.5 // indirect 11 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13 4 | before_install: 5 | - go get github.com/axw/gocov/gocov 6 | - go get github.com/mattn/goveralls 7 | - go get golang.org/x/lint/golint 8 | - go get golang.org/x/tools/cmd/cover 9 | before_script: 10 | - go get -d ./... 11 | script: 12 | - golint ./... 13 | - go vet ./... 14 | - go test -v ./... 15 | - if ! $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN; then echo "Coveralls not available."; fi 16 | - go build ./... 17 | -------------------------------------------------------------------------------- /rtorrentexporter_test.go: -------------------------------------------------------------------------------- 1 | package rtorrentexporter 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | ) 12 | 13 | func testCollector(t *testing.T, collector prometheus.Collector) []byte { 14 | if err := prometheus.Register(collector); err != nil { 15 | t.Fatalf("failed to register Prometheus collector: %v", err) 16 | } 17 | defer prometheus.Unregister(collector) 18 | 19 | promServer := httptest.NewServer(promhttp.Handler()) 20 | defer promServer.Close() 21 | 22 | resp, err := http.Get(promServer.URL) 23 | if err != nil { 24 | t.Fatalf("failed to GET data from prometheus: %v", err) 25 | } 26 | defer resp.Body.Close() 27 | 28 | buf, err := ioutil.ReadAll(resp.Body) 29 | if err != nil { 30 | t.Fatalf("failed to read server response: %v", err) 31 | } 32 | 33 | return buf 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2016 Matt Layher 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rtorrent_exporter [![GoDoc](http://godoc.org/github.com/mdlayher/rtorrent_exporter?status.svg)](http://godoc.org/github.com/mdlayher/rtorrent_exporter) [![Build Status](https://travis-ci.org/mdlayher/rtorrent_exporter.svg?branch=master)](https://travis-ci.org/mdlayher/rtorrent_exporter) [![Coverage Status](https://coveralls.io/repos/mdlayher/rtorrent_exporter/badge.svg?branch=master)](https://coveralls.io/r/mdlayher/rtorrent_exporter?branch=master) 2 | ================= 3 | 4 | Command `rtorrent_exporter` provides a Prometheus exporter for rTorrent. 5 | 6 | Package `rtorrentexporter` provides the Exporter type used in the `rtorrent_exporter` 7 | Prometheus exporter. 8 | 9 | MIT Licensed. 10 | 11 | Usage 12 | ----- 13 | 14 | Available flags for `rtorrent_exporter` include: 15 | 16 | ``` 17 | $ ./rtorrent_exporter -h 18 | Usage of ./rtorrent_exporter: 19 | -rtorrent.addr string 20 | address of rTorrent XML-RPC server 21 | -telemetry.addr string 22 | host:port for rTorrent exporter (default ":9135") 23 | -telemetry.path string 24 | URL path for surfacing collected metrics (default "/metrics") 25 | ``` 26 | 27 | An example of using `rtorrent_exporter`: 28 | 29 | ``` 30 | $ ./rtorrent_exporter -rtorrent.addr http://127.0.0.1/RPC2 31 | 2016/03/09 17:39:40 starting rTorrent exporter on ":9135" for server "http://127.0.0.1/RPC2" 32 | ``` 33 | 34 | Docker 35 | ------ 36 | 37 | ``` 38 | docker build -t rtorrent_exporter . 39 | docker run --rm -d -p 9135:9135 rtorrent_exporter -rtorrent.addr "http://127.0.0.1/RPC2" 40 | ``` 41 | 42 | Sample 43 | ------ 44 | 45 | Here is a screenshot of a sample dashboard created using [`grafana`](https://github.com/grafana/grafana) 46 | with metrics from exported from `rtorrent_exporter`. 47 | 48 | ![sample](https://cloud.githubusercontent.com/assets/1926905/13891308/bad263be-ed26-11e5-9601-9d770d95c538.png) 49 | -------------------------------------------------------------------------------- /rtorrentexporter.go: -------------------------------------------------------------------------------- 1 | // Package rtorrentexporter provides the Exporter type used in the 2 | // rtorrent_exporter Prometheus exporter. 3 | package rtorrentexporter 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/mdlayher/rtorrent" 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | const ( 13 | // namespace is the top-level namespace for this rTorrent exporter. 14 | namespace = "rtorrent" 15 | ) 16 | 17 | // An Exporter is a Prometheus exporter for rTorrent metrics. 18 | // It wraps all rTorrent metrics collectors and provides a single global 19 | // exporter which can serve metrics. It also ensures that the collection 20 | // is done in a thread-safe manner, the necessary requirement stated by 21 | // Prometheus. It implements the prometheus.Collector interface in order to 22 | // register with Prometheus. 23 | type Exporter struct { 24 | mu sync.Mutex 25 | collectors []prometheus.Collector 26 | } 27 | 28 | // Verify that the Exporter implements the prometheus.Collector interface. 29 | var _ prometheus.Collector = &Exporter{} 30 | 31 | // New creates a new Exporter which collects metrics from one or mote sites. 32 | func New(c *rtorrent.Client) *Exporter { 33 | return &Exporter{ 34 | collectors: []prometheus.Collector{ 35 | NewDownloadsCollector(c.Downloads), 36 | }, 37 | } 38 | } 39 | 40 | // Describe sends all the descriptors of the collectors included to 41 | // the provided channel. 42 | func (c *Exporter) Describe(ch chan<- *prometheus.Desc) { 43 | for _, cc := range c.collectors { 44 | cc.Describe(ch) 45 | } 46 | } 47 | 48 | // Collect sends the collected metrics from each of the collectors to 49 | // prometheus. Collect could be called several times concurrently 50 | // and thus its run is protected by a single mutex. 51 | func (c *Exporter) Collect(ch chan<- prometheus.Metric) { 52 | c.mu.Lock() 53 | defer c.mu.Unlock() 54 | 55 | for _, cc := range c.collectors { 56 | cc.Collect(ch) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/rtorrent_exporter/main.go: -------------------------------------------------------------------------------- 1 | // Command rtorrent_exporter provides a Prometheus exporter for rTorrent. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/mdlayher/rtorrent" 10 | "github.com/mdlayher/rtorrent_exporter" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | ) 14 | 15 | var ( 16 | telemetryAddr = flag.String("telemetry.addr", ":9135", "host:port for rTorrent exporter") 17 | metricsPath = flag.String("telemetry.path", "/metrics", "URL path for surfacing collected metrics") 18 | 19 | rtorrentAddr = flag.String("rtorrent.addr", "", "address of rTorrent XML-RPC server") 20 | rtorrentUsername = flag.String("rtorrent.username", "", "[optional] username used for HTTP Basic authentication with rTorrent XML-RPC server") 21 | rtorrentPassword = flag.String("rtorrent.password", "", "[optional] password used for HTTP Basic authentication with rTorrent XML-RPC server") 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if *rtorrentAddr == "" { 28 | log.Fatal("address of rTorrent XML-RPC server must be specified with '-rtorrent.addr' flag") 29 | } 30 | 31 | // Optionally enable HTTP Basic authentication 32 | var rt http.RoundTripper 33 | if u, p := *rtorrentUsername, *rtorrentPassword; u != "" && p != "" { 34 | rt = &authRoundTripper{ 35 | Username: u, 36 | Password: p, 37 | } 38 | } 39 | 40 | c, err := rtorrent.New(*rtorrentAddr, rt) 41 | if err != nil { 42 | log.Fatalf("cannot create rTorrent client: %v", err) 43 | } 44 | 45 | prometheus.MustRegister(rtorrentexporter.New(c)) 46 | 47 | http.Handle(*metricsPath, promhttp.Handler()) 48 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 49 | http.Redirect(w, r, *metricsPath, http.StatusMovedPermanently) 50 | }) 51 | 52 | log.Printf("starting rTorrent exporter on %q for server %q (authentication: %v)", 53 | *telemetryAddr, *rtorrentAddr, rt != nil) 54 | 55 | if err := http.ListenAndServe(*telemetryAddr, nil); err != nil { 56 | log.Fatalf("cannot start rTorrent exporter: %s", err) 57 | } 58 | } 59 | 60 | var _ http.RoundTripper = &authRoundTripper{} 61 | 62 | // An authRoundTripper is a http.RoundTripper which adds HTTP Basic authentication 63 | // to each HTTP request. 64 | type authRoundTripper struct { 65 | Username string 66 | Password string 67 | } 68 | 69 | func (rt *authRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 70 | r.SetBasicAuth(rt.Username, rt.Password) 71 | return http.DefaultTransport.RoundTrip(r) 72 | } 73 | -------------------------------------------------------------------------------- /downloadscollector_test.go: -------------------------------------------------------------------------------- 1 | package rtorrentexporter 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestDownloadsCollector(t *testing.T) { 10 | // Info-hashes are usually 40 bytes, but we will make ours only 11 | // 4 for easier testing 12 | hashA := strings.Repeat("A", 4) 13 | hashB := strings.Repeat("B", 4) 14 | hashC := strings.Repeat("C", 4) 15 | 16 | var tests = []struct { 17 | desc string 18 | ds *testDownloadsSource 19 | matches []*regexp.Regexp 20 | }{ 21 | { 22 | desc: "one download", 23 | ds: &testDownloadsSource{ 24 | downloads: []string{ 25 | hashA, 26 | }, 27 | files: map[string]string{ 28 | hashA: "foo", 29 | }, 30 | rate: 1024, 31 | total: 1024, 32 | }, 33 | matches: []*regexp.Regexp{ 34 | regexp.MustCompile(`rtorrent_downloads 1`), 35 | regexp.MustCompile(`rtorrent_downloads_started 1`), 36 | regexp.MustCompile(`rtorrent_downloads_stopped 1`), 37 | regexp.MustCompile(`rtorrent_downloads_complete 1`), 38 | regexp.MustCompile(`rtorrent_downloads_incomplete 1`), 39 | regexp.MustCompile(`rtorrent_downloads_hashing 1`), 40 | regexp.MustCompile(`rtorrent_downloads_seeding 1`), 41 | regexp.MustCompile(`rtorrent_downloads_leeching 1`), 42 | regexp.MustCompile(`rtorrent_downloads_active 1`), 43 | 44 | regexp.MustCompile(`rtorrent_downloads_download_rate_bytes{info_hash="AAAA",name="foo"} 1024`), 45 | regexp.MustCompile(`rtorrent_downloads_download_total_bytes{info_hash="AAAA",name="foo"} 1024`), 46 | regexp.MustCompile(`rtorrent_downloads_upload_rate_bytes{info_hash="AAAA",name="foo"} 1024`), 47 | regexp.MustCompile(`rtorrent_downloads_upload_total_bytes{info_hash="AAAA",name="foo"} 1024`), 48 | }, 49 | }, 50 | { 51 | desc: "three downloads", 52 | ds: &testDownloadsSource{ 53 | downloads: []string{ 54 | hashA, 55 | hashB, 56 | hashC, 57 | }, 58 | files: map[string]string{ 59 | hashA: "foo", 60 | hashB: "bar", 61 | hashC: "baz", 62 | }, 63 | rate: 2048, 64 | total: 2048, 65 | }, 66 | matches: []*regexp.Regexp{ 67 | regexp.MustCompile(`rtorrent_downloads 3`), 68 | regexp.MustCompile(`rtorrent_downloads_started 3`), 69 | regexp.MustCompile(`rtorrent_downloads_stopped 3`), 70 | regexp.MustCompile(`rtorrent_downloads_complete 3`), 71 | regexp.MustCompile(`rtorrent_downloads_incomplete 3`), 72 | regexp.MustCompile(`rtorrent_downloads_hashing 3`), 73 | regexp.MustCompile(`rtorrent_downloads_seeding 3`), 74 | regexp.MustCompile(`rtorrent_downloads_leeching 3`), 75 | regexp.MustCompile(`rtorrent_downloads_active 3`), 76 | 77 | regexp.MustCompile(`rtorrent_downloads_download_rate_bytes{info_hash="AAAA",name="foo"} 2048`), 78 | regexp.MustCompile(`rtorrent_downloads_download_total_bytes{info_hash="AAAA",name="foo"} 2048`), 79 | regexp.MustCompile(`rtorrent_downloads_upload_rate_bytes{info_hash="AAAA",name="foo"} 2048`), 80 | regexp.MustCompile(`rtorrent_downloads_upload_total_bytes{info_hash="AAAA",name="foo"} 2048`), 81 | regexp.MustCompile(`rtorrent_downloads_download_rate_bytes{info_hash="BBBB",name="bar"} 2048`), 82 | regexp.MustCompile(`rtorrent_downloads_download_total_bytes{info_hash="BBBB",name="bar"} 2048`), 83 | regexp.MustCompile(`rtorrent_downloads_upload_rate_bytes{info_hash="BBBB",name="bar"} 2048`), 84 | regexp.MustCompile(`rtorrent_downloads_upload_total_bytes{info_hash="BBBB",name="bar"} 2048`), 85 | regexp.MustCompile(`rtorrent_downloads_download_rate_bytes{info_hash="CCCC",name="baz"} 2048`), 86 | regexp.MustCompile(`rtorrent_downloads_download_total_bytes{info_hash="CCCC",name="baz"} 2048`), 87 | regexp.MustCompile(`rtorrent_downloads_upload_rate_bytes{info_hash="CCCC",name="baz"} 2048`), 88 | regexp.MustCompile(`rtorrent_downloads_upload_total_bytes{info_hash="CCCC",name="baz"} 2048`), 89 | }, 90 | }, 91 | } 92 | 93 | for i, tt := range tests { 94 | t.Logf("[%02d] test %q", i, tt.desc) 95 | 96 | out := testCollector(t, NewDownloadsCollector(tt.ds)) 97 | 98 | for j, m := range tt.matches { 99 | t.Logf("\t[%02d:%02d] match: %s", i, j, m.String()) 100 | 101 | if !m.Match(out) { 102 | t.Fatal("\toutput failed to match regex") 103 | } 104 | } 105 | } 106 | } 107 | 108 | var _ DownloadsSource = &testDownloadsSource{} 109 | 110 | type testDownloadsSource struct { 111 | downloads []string 112 | files map[string]string 113 | rate int 114 | total int 115 | } 116 | 117 | func (ds *testDownloadsSource) All() ([]string, error) { return ds.downloads, nil } 118 | func (ds *testDownloadsSource) Started() ([]string, error) { return ds.downloads, nil } 119 | func (ds *testDownloadsSource) Stopped() ([]string, error) { return ds.downloads, nil } 120 | func (ds *testDownloadsSource) Complete() ([]string, error) { return ds.downloads, nil } 121 | func (ds *testDownloadsSource) Incomplete() ([]string, error) { return ds.downloads, nil } 122 | func (ds *testDownloadsSource) Hashing() ([]string, error) { return ds.downloads, nil } 123 | func (ds *testDownloadsSource) Seeding() ([]string, error) { return ds.downloads, nil } 124 | func (ds *testDownloadsSource) Leeching() ([]string, error) { return ds.downloads, nil } 125 | func (ds *testDownloadsSource) Active() ([]string, error) { return ds.downloads, nil } 126 | 127 | func (ds *testDownloadsSource) BaseFilename(infoHash string) (string, error) { 128 | return ds.files[infoHash], nil 129 | } 130 | func (ds *testDownloadsSource) DownloadRate(_ string) (int, error) { return ds.rate, nil } 131 | func (ds *testDownloadsSource) DownloadTotal(_ string) (int, error) { return ds.total, nil } 132 | func (ds *testDownloadsSource) UploadRate(_ string) (int, error) { return ds.rate, nil } 133 | func (ds *testDownloadsSource) UploadTotal(_ string) (int, error) { return ds.total, nil } 134 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 4 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 6 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 12 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 13 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 14 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 15 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 16 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 20 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 22 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 24 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 25 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 26 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 27 | github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e h1:JZPIpxHmcXiQn101f6P9wkfRZs2A9268tHHnanj+esA= 28 | github.com/kolo/xmlrpc v0.0.0-20190909154602-56d5ec7c422e/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= 29 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 30 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 31 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 32 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 33 | github.com/mdlayher/rtorrent v0.0.0-20190602193728-e6d0c11730c7 h1:3576snTkyVrXsvWMCHPZW+BcZScsURLSaaINED6QGts= 34 | github.com/mdlayher/rtorrent v0.0.0-20190602193728-e6d0c11730c7/go.mod h1:/b7OFOB73g48SebJBe7XUsuRkV3ZcM2bFPs3BQvg8xE= 35 | github.com/mdlayher/rtorrent v0.0.0-20191011143022-58e7d496034e h1:U5Ad0UsquguX8AXoV0FRdoUbRwyGAOCxgpHH63KJRpU= 36 | github.com/mdlayher/rtorrent v0.0.0-20191011143022-58e7d496034e/go.mod h1:pu+ner2JS5stk0GIzbqha84Sq9uOU14p352NKWJuGM4= 37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 39 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 40 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 41 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 42 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 43 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 46 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 47 | github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= 48 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 49 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 50 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 51 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 52 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 53 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 54 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 55 | github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= 56 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 57 | github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= 58 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 59 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 60 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 61 | github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= 62 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 63 | github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= 64 | github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 65 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 66 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 70 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 71 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 72 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 73 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 74 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= 82 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= 84 | golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 86 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 89 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 91 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 95 | -------------------------------------------------------------------------------- /downloadscollector.go: -------------------------------------------------------------------------------- 1 | package rtorrentexporter 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/mdlayher/rtorrent" 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | var _ DownloadsSource = &rtorrent.DownloadService{} 11 | 12 | // A DownloadsSource is a type which can retrieve downloads information from 13 | // rTorrent. It is implemented by *rtorrent.DownloadService. 14 | type DownloadsSource interface { 15 | All() ([]string, error) 16 | Started() ([]string, error) 17 | Stopped() ([]string, error) 18 | Complete() ([]string, error) 19 | Incomplete() ([]string, error) 20 | Hashing() ([]string, error) 21 | Seeding() ([]string, error) 22 | Leeching() ([]string, error) 23 | Active() ([]string, error) 24 | 25 | BaseFilename(infoHash string) (string, error) 26 | DownloadRate(infoHash string) (int, error) 27 | DownloadTotal(infoHash string) (int, error) 28 | UploadRate(infoHash string) (int, error) 29 | UploadTotal(infoHash string) (int, error) 30 | } 31 | 32 | // A DownloadsCollector is a Prometheus collector for metrics regarding rTorrent 33 | // downloads. 34 | type DownloadsCollector struct { 35 | Downloads *prometheus.Desc 36 | DownloadsStarted *prometheus.Desc 37 | DownloadsStopped *prometheus.Desc 38 | DownloadsComplete *prometheus.Desc 39 | DownloadsIncomplete *prometheus.Desc 40 | DownloadsHashing *prometheus.Desc 41 | DownloadsSeeding *prometheus.Desc 42 | DownloadsLeeching *prometheus.Desc 43 | DownloadsActive *prometheus.Desc 44 | 45 | DownloadRateBytes *prometheus.Desc 46 | DownloadTotalBytes *prometheus.Desc 47 | UploadRateBytes *prometheus.Desc 48 | UploadTotalBytes *prometheus.Desc 49 | 50 | ds DownloadsSource 51 | } 52 | 53 | // Verify that DownloadsCollector implements the prometheus.Collector interface. 54 | var _ prometheus.Collector = &DownloadsCollector{} 55 | 56 | // NewDownloadsCollector creates a new DownloadsCollector which collects metrics 57 | // regarding rTorrent downloads. 58 | func NewDownloadsCollector(ds DownloadsSource) *DownloadsCollector { 59 | const ( 60 | subsystem = "downloads" 61 | ) 62 | 63 | var ( 64 | labels = []string{"info_hash", "name"} 65 | ) 66 | 67 | return &DownloadsCollector{ 68 | Downloads: prometheus.NewDesc( 69 | // Subsystem is used as name so we get "rtorrent_downloads" 70 | prometheus.BuildFQName(namespace, "", subsystem), 71 | "Total number of downloads.", 72 | nil, 73 | nil, 74 | ), 75 | 76 | DownloadsStarted: prometheus.NewDesc( 77 | prometheus.BuildFQName(namespace, subsystem, "started"), 78 | "Number of started downloads.", 79 | nil, 80 | nil, 81 | ), 82 | 83 | DownloadsStopped: prometheus.NewDesc( 84 | prometheus.BuildFQName(namespace, subsystem, "stopped"), 85 | "Number of stopped downloads.", 86 | nil, 87 | nil, 88 | ), 89 | 90 | DownloadsComplete: prometheus.NewDesc( 91 | prometheus.BuildFQName(namespace, subsystem, "complete"), 92 | "Number of complete downloads.", 93 | nil, 94 | nil, 95 | ), 96 | 97 | DownloadsIncomplete: prometheus.NewDesc( 98 | prometheus.BuildFQName(namespace, subsystem, "incomplete"), 99 | "Number of incomplete downloads.", 100 | nil, 101 | nil, 102 | ), 103 | 104 | DownloadsHashing: prometheus.NewDesc( 105 | prometheus.BuildFQName(namespace, subsystem, "hashing"), 106 | "Number of hashing downloads.", 107 | nil, 108 | nil, 109 | ), 110 | 111 | DownloadsSeeding: prometheus.NewDesc( 112 | prometheus.BuildFQName(namespace, subsystem, "seeding"), 113 | "Number of seeding downloads.", 114 | nil, 115 | nil, 116 | ), 117 | 118 | DownloadsLeeching: prometheus.NewDesc( 119 | prometheus.BuildFQName(namespace, subsystem, "leeching"), 120 | "Number of leeching downloads.", 121 | nil, 122 | nil, 123 | ), 124 | 125 | DownloadsActive: prometheus.NewDesc( 126 | prometheus.BuildFQName(namespace, subsystem, "active"), 127 | "Number of active downloads.", 128 | nil, 129 | nil, 130 | ), 131 | 132 | DownloadRateBytes: prometheus.NewDesc( 133 | prometheus.BuildFQName(namespace, subsystem, "download_rate_bytes"), 134 | "Current download rate in bytes.", 135 | labels, 136 | nil, 137 | ), 138 | 139 | DownloadTotalBytes: prometheus.NewDesc( 140 | prometheus.BuildFQName(namespace, subsystem, "download_total_bytes"), 141 | "Total Bytes downloaded.", 142 | labels, 143 | nil, 144 | ), 145 | 146 | UploadRateBytes: prometheus.NewDesc( 147 | prometheus.BuildFQName(namespace, subsystem, "upload_rate_bytes"), 148 | "Current upload rate in bytes.", 149 | labels, 150 | nil, 151 | ), 152 | 153 | UploadTotalBytes: prometheus.NewDesc( 154 | prometheus.BuildFQName(namespace, subsystem, "upload_total_bytes"), 155 | "Total Bytes uploaded.", 156 | labels, 157 | nil, 158 | ), 159 | 160 | ds: ds, 161 | } 162 | } 163 | 164 | // collect begins a metrics collection task for all metrics related to rTorrent 165 | // downloads. 166 | func (c *DownloadsCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { 167 | if desc, err := c.collectDownloadCounts(ch); err != nil { 168 | return desc, err 169 | } 170 | 171 | if desc, err := c.collectActiveDownloads(ch); err != nil { 172 | return desc, err 173 | } 174 | 175 | return nil, nil 176 | } 177 | 178 | // collectDownloadCounts collects metrics which track number of downloads in 179 | // various possible states. 180 | func (c *DownloadsCollector) collectDownloadCounts(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { 181 | all, err := c.ds.All() 182 | if err != nil { 183 | return c.Downloads, err 184 | } 185 | 186 | started, err := c.ds.Started() 187 | if err != nil { 188 | return c.DownloadsStarted, err 189 | } 190 | 191 | stopped, err := c.ds.Stopped() 192 | if err != nil { 193 | return c.DownloadsStopped, err 194 | } 195 | 196 | complete, err := c.ds.Complete() 197 | if err != nil { 198 | return c.DownloadsComplete, err 199 | } 200 | 201 | incomplete, err := c.ds.Incomplete() 202 | if err != nil { 203 | return c.DownloadsIncomplete, err 204 | } 205 | 206 | hashing, err := c.ds.Hashing() 207 | if err != nil { 208 | return c.DownloadsHashing, err 209 | } 210 | 211 | seeding, err := c.ds.Seeding() 212 | if err != nil { 213 | return c.DownloadsSeeding, err 214 | } 215 | 216 | leeching, err := c.ds.Leeching() 217 | if err != nil { 218 | return c.DownloadsLeeching, err 219 | } 220 | 221 | ch <- prometheus.MustNewConstMetric( 222 | c.Downloads, 223 | prometheus.GaugeValue, 224 | float64(len(all)), 225 | ) 226 | 227 | ch <- prometheus.MustNewConstMetric( 228 | c.DownloadsStarted, 229 | prometheus.GaugeValue, 230 | float64(len(started)), 231 | ) 232 | 233 | ch <- prometheus.MustNewConstMetric( 234 | c.DownloadsStopped, 235 | prometheus.GaugeValue, 236 | float64(len(stopped)), 237 | ) 238 | 239 | ch <- prometheus.MustNewConstMetric( 240 | c.DownloadsComplete, 241 | prometheus.GaugeValue, 242 | float64(len(complete)), 243 | ) 244 | 245 | ch <- prometheus.MustNewConstMetric( 246 | c.DownloadsIncomplete, 247 | prometheus.GaugeValue, 248 | float64(len(incomplete)), 249 | ) 250 | 251 | ch <- prometheus.MustNewConstMetric( 252 | c.DownloadsHashing, 253 | prometheus.GaugeValue, 254 | float64(len(hashing)), 255 | ) 256 | 257 | ch <- prometheus.MustNewConstMetric( 258 | c.DownloadsSeeding, 259 | prometheus.GaugeValue, 260 | float64(len(seeding)), 261 | ) 262 | 263 | ch <- prometheus.MustNewConstMetric( 264 | c.DownloadsLeeching, 265 | prometheus.GaugeValue, 266 | float64(len(leeching)), 267 | ) 268 | 269 | return nil, nil 270 | } 271 | 272 | // collectActiveDownloads collects information about active downloads, 273 | // which are uploading and/or downloading data. 274 | func (c *DownloadsCollector) collectActiveDownloads(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { 275 | active, err := c.ds.Active() 276 | if err != nil { 277 | return c.DownloadsActive, err 278 | } 279 | 280 | ch <- prometheus.MustNewConstMetric( 281 | c.DownloadsActive, 282 | prometheus.GaugeValue, 283 | float64(len(active)), 284 | ) 285 | 286 | for _, a := range active { 287 | name, err := c.ds.BaseFilename(a) 288 | if err != nil { 289 | return c.DownloadRateBytes, err 290 | } 291 | 292 | labels := []string{ 293 | a, 294 | name, 295 | } 296 | 297 | down, err := c.ds.DownloadRate(a) 298 | if err != nil { 299 | return c.DownloadRateBytes, err 300 | } 301 | 302 | downTotal, err := c.ds.DownloadTotal(a) 303 | if err != nil { 304 | return c.DownloadTotalBytes, err 305 | } 306 | 307 | up, err := c.ds.UploadRate(a) 308 | if err != nil { 309 | return c.UploadRateBytes, err 310 | } 311 | 312 | upTotal, err := c.ds.UploadTotal(a) 313 | if err != nil { 314 | return c.UploadTotalBytes, err 315 | } 316 | 317 | ch <- prometheus.MustNewConstMetric( 318 | c.DownloadRateBytes, 319 | prometheus.GaugeValue, 320 | float64(down), 321 | labels..., 322 | ) 323 | 324 | ch <- prometheus.MustNewConstMetric( 325 | c.DownloadTotalBytes, 326 | prometheus.GaugeValue, 327 | float64(downTotal), 328 | labels..., 329 | ) 330 | 331 | ch <- prometheus.MustNewConstMetric( 332 | c.UploadRateBytes, 333 | prometheus.GaugeValue, 334 | float64(up), 335 | labels..., 336 | ) 337 | 338 | ch <- prometheus.MustNewConstMetric( 339 | c.UploadTotalBytes, 340 | prometheus.GaugeValue, 341 | float64(upTotal), 342 | labels..., 343 | ) 344 | } 345 | 346 | return nil, nil 347 | } 348 | 349 | // Describe sends the descriptors of each metric over to the provided channel. 350 | // The corresponding metric values are sent separately. 351 | func (c *DownloadsCollector) Describe(ch chan<- *prometheus.Desc) { 352 | ds := []*prometheus.Desc{ 353 | c.Downloads, 354 | c.DownloadsStarted, 355 | c.DownloadsStopped, 356 | c.DownloadsComplete, 357 | c.DownloadsIncomplete, 358 | c.DownloadsHashing, 359 | c.DownloadsSeeding, 360 | c.DownloadsLeeching, 361 | c.DownloadsActive, 362 | 363 | c.DownloadRateBytes, 364 | c.DownloadTotalBytes, 365 | c.UploadRateBytes, 366 | c.UploadTotalBytes, 367 | } 368 | 369 | for _, d := range ds { 370 | ch <- d 371 | } 372 | } 373 | 374 | // Collect sends the metric values for each metric pertaining to the rTorrent 375 | // downloads to the provided prometheus Metric channel. 376 | func (c *DownloadsCollector) Collect(ch chan<- prometheus.Metric) { 377 | if desc, err := c.collect(ch); err != nil { 378 | log.Printf("[ERROR] failed collecting download metric %v: %v", desc, err) 379 | ch <- prometheus.NewInvalidMetric(desc, err) 380 | return 381 | } 382 | } 383 | --------------------------------------------------------------------------------