diff --git a/README.md b/README.md index dc45774..8211f60 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,26 @@ [![Build Status](https://cloud.drone.io/api/badges/xsteadfastx/jitsiexporter/status.svg)](https://cloud.drone.io/xsteadfastx/jitsiexporter) [![Docker Repository on Quay](https://quay.io/repository/xsteadfastx/jitsiexporter/status "Docker Repository on Quay")](https://quay.io/repository/xsteadfastx/jitsiexporter) -a jitsi meet prometheus exporter +A Jitsi meet prometheus exporter. + + Usage of ./jitsiexporter_linux_amd64: + -debug + Enable debug. + -host string + Host to listen on. (default "localhost") + -interval duration + Seconds to wait before scraping. (default 30s) + -port int + Port to listen on. (default 9700) + -url string + URL of Jitsi Videobridge Colibri Stats. + -version + Prints version. + +## Usage + +For a docker based setup, you can use the docker image [quay.io/xsteadfastx/jitsiexporter](https://quay.io/repository/xsteadfastx/jitsiexporter). + +1. [Enable](https://github.com/jitsi/jitsi-videobridge/blob/master/doc/statistics.md) `/colibri/stats` for the Jitsi videobridge. When you use the Jitsi docker setup use environment variable `JVB_ENABLE_APIS=rest,colibri`. +2. Be sure that the exporter and the videobridge API can communicate. In the docker Jitsi setup: Add the `jitsiexporter` to the `jitsi-meet_meet.jitsi`-network. The url would be `http://jitsi-meet_jvb_1:8080`. +3. A failed scrape metric is exporter as `jitsi_fetch_errors`. diff --git a/cmd/jitsiexporter/main.go b/cmd/jitsiexporter/main.go index 7b8dcec..246c7d5 100644 --- a/cmd/jitsiexporter/main.go +++ b/cmd/jitsiexporter/main.go @@ -15,7 +15,7 @@ func main() { ver := flag.Bool("version", false, "Prints version.") url := flag.String("url", "", "URL of Jitsi Videobridge Colibri Stats.") debug := flag.Bool("debug", false, "Enable debug.") - interval := flag.Duration("interval", 10*time.Second, "Seconds to wait before scraping.") // nolint: gomnd + interval := flag.Duration("interval", 30*time.Second, "Seconds to wait before scraping.") // nolint: gomnd port := flag.Int("port", 9700, "Port to listen on.") host := flag.String("host", "localhost", "Host to listen on.") flag.Parse() diff --git a/jitsiexporter.go b/jitsiexporter.go index 2e7dbf1..0dd823c 100644 --- a/jitsiexporter.go +++ b/jitsiexporter.go @@ -1,6 +1,7 @@ package jitsiexporter import ( + "context" "encoding/json" "fmt" "net/http" @@ -20,17 +21,32 @@ type Metric struct { } type Metrics struct { - Metrics map[string]Metric - URL string - Stater Stater - mux sync.Mutex + Metrics map[string]Metric + URL string + Stater Stater + mux sync.Mutex + Errors prometheus.Counter + Interval time.Duration } -func (m *Metrics) Update() { - now := m.Stater.Now(m.URL) +func (m *Metrics) Update() error { + m.mux.Lock() + defer m.mux.Unlock() + + now, err := m.Stater.Now(m.URL) + + if err != nil { + m.Errors.Inc() + + for _, i := range m.Metrics { + prometheus.Unregister(i.Gauge) + } + + return err + } + log.Debug(now) - m.mux.Lock() for k, v := range now { fieldLogger := log.WithFields(log.Fields{"key": k}) @@ -65,48 +81,99 @@ func (m *Metrics) Update() { continue } } - m.mux.Unlock() + + return nil +} + +type Response struct { + Resp *http.Response + Error error +} + +func get(ctx context.Context, url string, resp chan Response) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + resp <- Response{Resp: nil, Error: err} + return + } + + client := http.DefaultClient + + res, err := client.Do(req.WithContext(ctx)) // nolint:bodyclose + if err != nil { + resp <- Response{Resp: nil, Error: err} + } + + resp <- Response{Resp: res, Error: nil} } type Stater interface { - Now(url string) map[string]interface{} + Now(url string) (map[string]interface{}, error) } type colibri struct{} -func (c colibri) Now(url string) map[string]interface{} { +func (c colibri) Now(url string) (map[string]interface{}, error) { s := make(map[string]interface{}) - resp, err := http.Get(url) // nolint:gosec + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // nolint:gomnd + + defer cancel() + + res := make(chan Response) + + var resp *http.Response + + var err error + + go get(ctx, url, res) + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case r := <-res: + err = r.Error + resp = r.Resp + + defer resp.Body.Close() + } if err != nil { - log.Fatal(err) + return nil, err } - defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&s) if err != nil { - log.Fatal(err) + return nil, err } - return s + return s, nil } func collect(m *Metrics) { for { - m.Update() - time.Sleep(30 * time.Second) // nolint:gomnd + err := m.Update() + if err != nil { + log.Error(err) + } + + time.Sleep(m.Interval) // nolint:gomnd } } func Serve(url string, debug bool, interval time.Duration, port int, host string) { s := colibri{} + e := prometheus.NewCounter(prometheus.CounterOpts{Name: "jitsi_fetch_errors"}) metrics := &Metrics{ - URL: url, - Stater: s, - Metrics: make(map[string]Metric), + URL: url, + Stater: s, + Metrics: make(map[string]Metric), + Errors: e, + Interval: interval, } + prometheus.MustRegister(e) + if debug { log.SetLevel(log.DebugLevel) } diff --git a/main_test.go b/main_test.go index eeaac1b..474414f 100644 --- a/main_test.go +++ b/main_test.go @@ -16,7 +16,7 @@ func TestUpdate(t *testing.T) { s["bar"] = 1 // nolint:gomnd s["zonk"] = float64(1) // nolint:gomnd mockStater := &MockStater{} - mockStater.On("Now", "http://foo.tld").Return(s) + mockStater.On("Now", "http://foo.tld").Return(s, nil) m := &Metrics{ URL: "http://foo.tld", diff --git a/mock_Stater.go b/mock_Stater.go index fb1f973..d21e392 100644 --- a/mock_Stater.go +++ b/mock_Stater.go @@ -10,7 +10,7 @@ type MockStater struct { } // Now provides a mock function with given fields: url -func (_m *MockStater) Now(url string) map[string]interface{} { +func (_m *MockStater) Now(url string) (map[string]interface{}, error) { ret := _m.Called(url) var r0 map[string]interface{} @@ -22,5 +22,12 @@ func (_m *MockStater) Now(url string) map[string]interface{} { } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 }