diff --git a/README.md b/README.md index f8d6165..649556e 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ FTTH subscription. This exporter currently exposes the following metrics: -| Name | Type | Description | Labels | -| -------------------------- | ----- | ---------------------------- | --------- | -| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | -| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | +| Name | Type | Description | Labels | +| -------------------------- | ----- | ---------------------------------- | --------- | +| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | +| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | +| livebox_devices_total | gauge | The total number of active devices | type | ## Usage diff --git a/go.mod b/go.mod index 581ecb4..06fedfe 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/Tomy2e/livebox-exporter go 1.17 require ( - github.com/Tomy2e/livebox-api-client v0.0.0-20211112014400-2ac195fb56d7 + github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7 github.com/prometheus/client_golang v1.11.0 + golang.org/x/sync v0.1.0 ) require ( diff --git a/go.sum b/go.sum index 0ed74c7..8f97506 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/Tomy2e/livebox-api-client v0.0.0-20211112014400-2ac195fb56d7 h1:7rtUzWI31OT0PE51M1wP1IQNV2PwMXv5kTRmUDyS2QU= -github.com/Tomy2e/livebox-api-client v0.0.0-20211112014400-2ac195fb56d7/go.mod h1:3uvJQHRP5V3BKPTzWCOmUMxZrPbJNl45Wu7ueX9L8QM= +github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7 h1:aX04myQJxWqjP1I1S8jiE8fKyXSu4CHKMBOCUMTwlCI= +github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7/go.mod h1:3uvJQHRP5V3BKPTzWCOmUMxZrPbJNl45Wu7ueX9L8QM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -104,6 +104,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/poller/devices.go b/internal/poller/devices.go new file mode 100644 index 0000000..a244969 --- /dev/null +++ b/internal/poller/devices.go @@ -0,0 +1,63 @@ +package poller + +import ( + "context" + "fmt" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &DevicesTotal{} + +// DevicesTotal allows to poll the total number of active devices. +type DevicesTotal struct { + client livebox.Client + devicesTotal *prometheus.GaugeVec +} + +// NewDevicesTotal returns a new DevicesTotal poller. +func NewDevicesTotal(client livebox.Client) *DevicesTotal { + return &DevicesTotal{ + client: client, + devicesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_devices_total", + Help: "The total number of active devices", + }, []string{ + // Type of the device (dongle, ethernet, printer or wifi). + "type", + }), + } +} + +// Collectors returns all metrics. +func (dt *DevicesTotal) Collectors() []prometheus.Collector { + return []prometheus.Collector{dt.devicesTotal} +} + +// Poll polls the current number of active devices. +func (dt *DevicesTotal) Poll(ctx context.Context) error { + var devices struct { + Status map[string][]struct{} `json:"status"` + } + + if err := dt.client.Request(ctx, request.New("Devices", "get", map[string]interface{}{ + "expression": map[string]string{ + "ethernet": "not interface and not self and eth and .Active==true", + "wifi": "not interface and not self and wifi and .Active==true", + "printer": "printer and .Active==true", + "dongle": "usb && wwan and .Active==true", + }, + }), &devices); err != nil { + return fmt.Errorf("failed to get active devices: %w", err) + } + + for t, d := range devices.Status { + dt.devicesTotal. + With(prometheus.Labels{"type": t}). + Set(float64(len(d))) + } + + return nil +} diff --git a/internal/poller/interface.go b/internal/poller/interface.go new file mode 100644 index 0000000..2140891 --- /dev/null +++ b/internal/poller/interface.go @@ -0,0 +1,97 @@ +package poller + +import ( + "context" + "fmt" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &InterfaceMbits{} + +// InterfaceMbits allows to poll the current bandwidth usage on the Livebox +// interfaces. +type InterfaceMbits struct { + client livebox.Client + txMbits, rxMbits *prometheus.GaugeVec +} + +// NewInterfaceMbits returns a new InterfaceMbits poller. +func NewInterfaceMbits(client livebox.Client) *InterfaceMbits { + return &InterfaceMbits{ + client: client, + txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_tx_mbits", + Help: "Transmitted Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_rx_mbits", + Help: "Received Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + } +} + +// Collectors returns all metrics. +func (im *InterfaceMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{im.txMbits, im.rxMbits} +} + +func bitsPer30SecsToMbitsPerSec(v int) float64 { + return float64(v) / 30000000 +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceMbits) Poll(ctx context.Context) error { + var counters struct { + Status map[string]struct { + Traffic []struct { + Timestamp int `json:"Timestamp"` + RxCounter int `json:"Rx_Counter"` + TxCounter int `json:"Tx_Counter"` + } `json:"Traffic"` + } `json:"status"` + } + + // Request latest rx/tx counters. + if err := im.client.Request( + ctx, + request.New( + "HomeLan", + "getResults", + map[string]interface{}{ + "Seconds": 0, + "NumberOfReadings": 1, + }, + ), + &counters, + ); err != nil { + return fmt.Errorf("failed to get interfaces: %w", err) + } + + for iface, traffic := range counters.Status { + rxCounter := 0 + txCounter := 0 + + if len(traffic.Traffic) > 0 { + rxCounter = traffic.Traffic[0].RxCounter + txCounter = traffic.Traffic[0].TxCounter + } + + im.rxMbits. + With(prometheus.Labels{"interface": iface}). + Set(bitsPer30SecsToMbitsPerSec(rxCounter)) + im.txMbits. + With(prometheus.Labels{"interface": iface}). + Set(bitsPer30SecsToMbitsPerSec(txCounter)) + } + + return nil +} diff --git a/internal/poller/poller.go b/internal/poller/poller.go new file mode 100644 index 0000000..b126c78 --- /dev/null +++ b/internal/poller/poller.go @@ -0,0 +1,41 @@ +package poller + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sync/errgroup" +) + +// Poller is an interface that allows polling a system and updating Prometheus +// metrics. +type Poller interface { + Poll(ctx context.Context) error + Collectors() []prometheus.Collector +} + +// Pollers is a list of pollers. +type Pollers []Poller + +// Collectors returns the collectors of all pollers. +func (p Pollers) Collectors() (c []prometheus.Collector) { + for _, poller := range p { + c = append(c, poller.Collectors()...) + } + + return +} + +// Poll runs all pollers in parallel. +func (p Pollers) Poll(ctx context.Context) error { + eg, ctx := errgroup.WithContext(ctx) + + for _, poller := range p { + poller := poller + eg.Go(func() error { + return poller.Poll(ctx) + }) + } + + return eg.Wait() +} diff --git a/main.go b/main.go index e221dad..3f67a1e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "log" "net/http" @@ -9,35 +10,14 @@ import ( "time" "github.com/Tomy2e/livebox-api-client" - "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/internal/poller" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" ) const defaultPollingFrequency = 30 -var ( - rxMbits = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "livebox_interface_rx_mbits", - Help: "Received Mbits per second.", - }, []string{ - // Name of the interface. - "interface", - }) - txMbits = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "livebox_interface_tx_mbits", - Help: "Transmitted Mbits per second.", - }, []string{ - // Name of the interface. - "interface", - }) -) - -func bitsPer30SecsToMbitsPerSec(v int) float64 { - return float64(v) / 30000000 -} - func main() { pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency") listen := flag.String("listen", ":8080", "Listening address") @@ -48,59 +28,41 @@ func main() { log.Fatal("ADMIN_PASSWORD environment variable must be set") } - ctx := context.Background() - client := livebox.NewClient(adminPassword) + var ( + ctx = context.Background() + registry = prometheus.NewRegistry() + client = livebox.NewClient(adminPassword) + pollers = poller.Pollers{ + poller.NewDevicesTotal(client), + poller.NewInterfaceMbits(client), + } + ) + + registry.MustRegister( + append( + pollers.Collectors(), + collectors.NewGoCollector(), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), + )..., + ) go func() { for { - var counters struct { - Status map[string]struct { - Traffic []struct { - Timestamp int `json:"Timestamp"` - RxCounter int `json:"Rx_Counter"` - TxCounter int `json:"Tx_Counter"` - } `json:"Traffic"` - } `json:"status"` - } - - // Request latest rx/tx counters. - if err := client.Request( - ctx, - request.New( - "HomeLan", - "getResults", - map[string]interface{}{ - "Seconds": 0, - "NumberOfReadings": 1, - }, - ), - &counters, - ); err != nil { - log.Fatalf("Request to Livebox API failed: %v", err) - } - - for iface, traffic := range counters.Status { - rxCounter := 0 - txCounter := 0 - - if len(traffic.Traffic) > 0 { - rxCounter = traffic.Traffic[0].RxCounter - txCounter = traffic.Traffic[0].TxCounter + if err := pollers.Poll(ctx); err != nil { + if errors.Is(err, livebox.ErrInvalidPassword) { + log.Fatal(err) } - rxMbits. - With(prometheus.Labels{"interface": iface}). - Set(bitsPer30SecsToMbitsPerSec(rxCounter)) - txMbits. - With(prometheus.Labels{"interface": iface}). - Set(bitsPer30SecsToMbitsPerSec(txCounter)) + log.Printf("WARN: polling failed: %s\n", err) } time.Sleep(time.Duration(*pollingFrequency) * time.Second) } }() - http.Handle("/metrics", promhttp.Handler()) + http.Handle("/metrics", promhttp.InstrumentMetricHandler( + registry, promhttp.HandlerFor(registry, promhttp.HandlerOpts{}), + )) log.Printf("Listening on %s\n", *listen) log.Fatal(http.ListenAndServe(*listen, nil)) }