From 6cf0fa27ecf4b03bb8e19e6db1353b0bb57622a3 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Sun, 5 Mar 2023 14:50:14 +0100 Subject: [PATCH 1/8] Implement two new ways to get metrics in InterfaceMbits --- internal/poller/interface.go | 272 ++++++++++++++++++++++++++++++----- main.go | 2 +- 2 files changed, 236 insertions(+), 38 deletions(-) diff --git a/internal/poller/interface.go b/internal/poller/interface.go index 2140891..d1ba405 100644 --- a/internal/poller/interface.go +++ b/internal/poller/interface.go @@ -2,11 +2,15 @@ package poller import ( "context" + "errors" "fmt" + "strings" + "time" "github.com/Tomy2e/livebox-api-client" "github.com/Tomy2e/livebox-api-client/api/request" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sync/errgroup" ) var _ Poller = &InterfaceMbits{} @@ -14,8 +18,26 @@ 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 + client livebox.Client + txMbits, rxMbits *prometheus.GaugeVec + txMbitsNetDev, rxMbitsNetDev *prometheus.GaugeVec + bytesSent, bytesReceived *prometheus.CounterVec + interfaces map[string]netInterface + interfacesNetDev map[string]netInterface +} + +type netInterface struct { + Flags string + LastTx, LastRx int64 + LastPoll time.Time +} + +func (ni *netInterface) IsWAN() bool { + return strings.Contains(ni.Flags, "wan") +} + +func (ni *netInterface) IsWLAN() bool { + return strings.Contains(ni.Flags, "wlanvap") } // NewInterfaceMbits returns a new InterfaceMbits poller. @@ -36,61 +58,237 @@ func NewInterfaceMbits(client livebox.Client) *InterfaceMbits { // Name of the interface. "interface", }), + txMbitsNetDev: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_tx_mbits", + Help: "Transmitted Mbits per second, calculated from netdevstats.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbitsNetDev: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_rx_mbits", + Help: "Received Mbits per second, calculated from netdevstats.", + }, []string{ + // Name of the interface. + "interface", + }), + bytesSent: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "livebox_interface_bytes_sent_total", + Help: "Bytes sent on the interface", + }, []string{ + "interface", + }), + bytesReceived: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "livebox_interface_bytes_received_total", + Help: "Bytes received on the interface", + }, []string{ + "interface", + }), + interfaces: make(map[string]netInterface), + interfacesNetDev: make(map[string]netInterface), } } // Collectors returns all metrics. func (im *InterfaceMbits) Collectors() []prometheus.Collector { - return []prometheus.Collector{im.txMbits, im.rxMbits} + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + im.txMbitsNetDev, + im.rxMbitsNetDev, + im.bytesSent, + im.bytesReceived, + } } 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"` +func (im *InterfaceMbits) discoverInterfaces(ctx context.Context) error { + var mibs struct { + Status struct { + //GPON map[string]struct{} `json:"gpon"` + Base map[string]struct { + Flags string `json:"flags"` + } `json:"base"` } `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, + request.New("NeMo.Intf.data", "getMIBs", map[string]interface{}{ + "traverse": "all", + "flag": "statmon && !vlan", + }), + &mibs, ); err != nil { - return fmt.Errorf("failed to get interfaces: %w", err) + return fmt.Errorf("failed to discover interface: %w", err) } - for iface, traffic := range counters.Status { - rxCounter := 0 - txCounter := 0 + if len(mibs.Status.Base) == 0 { + return errors.New("wan interface not found") + } - 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)) + for itf, val := range mibs.Status.Base { + im.interfaces[itf] = netInterface{Flags: val.Flags} + im.interfacesNetDev[itf] = netInterface{Flags: val.Flags} + } + + return nil +} + +func bytesPerSecToMbits(bytes float64) float64 { + return bytes * 8 / 1000000 +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceMbits) Poll(ctx context.Context) error { + if len(im.interfaces) == 0 { + if err := im.discoverInterfaces(ctx); err != nil { + return err + } + } + + eg, ctx := errgroup.WithContext(ctx) + + eg.Go(func() error { + return im.pollInterfaces(ctx) + }) + + eg.Go(func() error { + return im.pollInterfacesNetDev(ctx) + }) + + return eg.Wait() +} + +func (im *InterfaceMbits) pollInterfaces(ctx context.Context) error { + for itf, val := range im.interfaces { + elapsed := time.Now().Sub(val.LastPoll) + if elapsed.Seconds() < 30 { + // Polling must only be done once every 30 seconds Livebox updates data + // only every 30 seconds. + continue + } + + var stats struct { + Status struct { + BytesReceived int64 `json:"BytesReceived"` + BytesSent int64 `json:"BytesSent"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("HomeLan.Interface.%s.Stats", itf), + "get", + nil, + ), &stats); err != nil { + return err + } + + rxMetric := im.rxMbits + txMetric := im.txMbits + brMetric := im.bytesReceived + bsMetric := im.bytesSent + + if !val.IsWAN() { + rxMetric = im.txMbits + txMetric = im.rxMbits + brMetric = im.bytesSent + bsMetric = im.bytesReceived + } + + if !val.LastPoll.IsZero() { + if elapsed.Seconds() > 0 { + if stats.Status.BytesReceived >= val.LastRx { + diff := float64(stats.Status.BytesReceived - val.LastRx) + rxMetric. + With(prometheus.Labels{"interface": itf}). + Set(bytesPerSecToMbits(diff / (elapsed.Seconds()))) + brMetric.With(prometheus.Labels{"interface": itf}).Add(diff) + } else { + // Counter was reset? + brMetric.Reset() + brMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesReceived)) + } + + if stats.Status.BytesSent >= val.LastTx { + diff := float64(stats.Status.BytesSent - val.LastTx) + txMetric. + With(prometheus.Labels{"interface": itf}). + Set(bytesPerSecToMbits(diff / (elapsed.Seconds()))) + bsMetric.With(prometheus.Labels{"interface": itf}).Add(diff) + + } else { + // Counter was reset? + bsMetric.Reset() + bsMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesSent)) + } + } + } else { + // Initialize bytes + bsMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesSent)) + brMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesReceived)) + } + + val.LastTx = stats.Status.BytesSent + val.LastRx = stats.Status.BytesReceived + + val.LastPoll = time.Now() + im.interfaces[itf] = val + } + + return nil +} + +func (im *InterfaceMbits) pollInterfacesNetDev(ctx context.Context) error { + for itf, val := range im.interfacesNetDev { + var stats struct { + Status struct { + RxBytes int64 `json:"RxBytes"` + TxBytes int64 `json:"TxBytes"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("NeMo.Intf.%s", itf), + "getNetDevStats", + nil, + ), &stats); err != nil { + return err + } + + rxMetric := im.rxMbitsNetDev + txMetric := im.txMbitsNetDev + + if !val.IsWAN() { + rxMetric = im.txMbitsNetDev + txMetric = im.rxMbitsNetDev + } + + if !val.LastPoll.IsZero() { + elapsed := time.Now().Sub(val.LastPoll) + if elapsed.Seconds() > 0 { + if stats.Status.RxBytes >= val.LastRx { + rxMetric. + With(prometheus.Labels{"interface": itf}). + Set(8 * float64(stats.Status.RxBytes-val.LastRx) / (elapsed.Seconds() * 1000000)) + } + + if stats.Status.TxBytes >= val.LastTx { + txMetric. + With(prometheus.Labels{"interface": itf}). + Set(8 * float64(stats.Status.TxBytes-val.LastTx) / (elapsed.Seconds() * 1000000)) + } + + } + } + + val.LastRx = stats.Status.RxBytes + val.LastTx = stats.Status.TxBytes + val.LastPoll = time.Now() + im.interfacesNetDev[itf] = val } return nil diff --git a/main.go b/main.go index 3f67a1e..5d4ad7e 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) -const defaultPollingFrequency = 30 +const defaultPollingFrequency = 5 func main() { pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency") From b008540c3f913349a0b33fa6401cd096543b247f Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Sat, 18 Mar 2023 18:56:02 +0100 Subject: [PATCH 2/8] Set new metrics as experimental --- README.md | 28 ++- go.mod | 3 +- go.sum | 6 +- internal/poller/common.go | 9 + internal/poller/interface.go | 279 ++++----------------------- internal/poller/interface_homelan.go | 110 +++++++++++ internal/poller/interface_netdev.go | 99 ++++++++++ internal/poller/wan.go | 81 ++++++++ main.go | 86 ++++++++- pkg/bitrate/bitrate.go | 106 ++++++++++ pkg/bitrate/conversion.go | 11 ++ pkg/livebox/discovery.go | 64 ++++++ 12 files changed, 629 insertions(+), 253 deletions(-) create mode 100644 internal/poller/common.go create mode 100644 internal/poller/interface_homelan.go create mode 100644 internal/poller/interface_netdev.go create mode 100644 internal/poller/wan.go create mode 100644 pkg/bitrate/bitrate.go create mode 100644 pkg/bitrate/conversion.go create mode 100644 pkg/livebox/discovery.go diff --git a/README.md b/README.md index 649556e..71df353 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,20 @@ 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 | -| livebox_devices_total | gauge | The total number of active devices | type | +| Name | Type | Description | Labels | Experimental | +| ---------------------------------- | ----- | ------------------------------------------------- | --------- | ------------ | +| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | No | +| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | No | +| livebox_devices_total | gauge | The total number of active devices | type | No | +| livebox_interface_homelan_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_homelan_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_interface_netdev_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_netdev_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | interface | Yes | +| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | interface | Yes | + +Experimental metrics are not enabled by default, use the `-experimental` +command-line option to enable them. ## Usage @@ -19,10 +28,11 @@ This exporter currently exposes the following metrics: The exporter accepts the following command-line options: -| Name | Description | Default value | -| ------------------- | ----------------- | ------------- | -| --polling-frequency | Polling frequency | 30 | -| --listen | Listening address | :8080 | +| Name | Description | Default value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| -polling-frequency | Polling frequency | 30 | +| -listen | Listening address | :8080 | +| -experimental | Comma separated list of experimental metrics to enable (available metrics: livebox_interface_homelan,livebox_interface_netdev,livebox_wan) | | The exporter reads the following environment variables: diff --git a/go.mod b/go.mod index 7e83d4f..f497aca 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7 github.com/prometheus/client_golang v1.14.0 + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 golang.org/x/sync v0.1.0 ) @@ -16,6 +17,6 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/sys v0.1.0 // indirect google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 1a55841..acacb92 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -325,8 +327,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/poller/common.go b/internal/poller/common.go new file mode 100644 index 0000000..b4f004b --- /dev/null +++ b/internal/poller/common.go @@ -0,0 +1,9 @@ +package poller + +import "math" + +const maxMbits = 2150 + +func sanitizeMbits(mbits float64) float64 { + return math.Min(mbits, maxMbits) +} diff --git a/internal/poller/interface.go b/internal/poller/interface.go index d1ba405..03c152e 100644 --- a/internal/poller/interface.go +++ b/internal/poller/interface.go @@ -2,15 +2,12 @@ package poller import ( "context" - "errors" "fmt" - "strings" - "time" "github.com/Tomy2e/livebox-api-client" "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" "github.com/prometheus/client_golang/prometheus" - "golang.org/x/sync/errgroup" ) var _ Poller = &InterfaceMbits{} @@ -18,26 +15,8 @@ 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 - txMbitsNetDev, rxMbitsNetDev *prometheus.GaugeVec - bytesSent, bytesReceived *prometheus.CounterVec - interfaces map[string]netInterface - interfacesNetDev map[string]netInterface -} - -type netInterface struct { - Flags string - LastTx, LastRx int64 - LastPoll time.Time -} - -func (ni *netInterface) IsWAN() bool { - return strings.Contains(ni.Flags, "wan") -} - -func (ni *netInterface) IsWLAN() bool { - return strings.Contains(ni.Flags, "wlanvap") + client livebox.Client + txMbits, rxMbits *prometheus.GaugeVec } // NewInterfaceMbits returns a new InterfaceMbits poller. @@ -58,237 +37,57 @@ func NewInterfaceMbits(client livebox.Client) *InterfaceMbits { // Name of the interface. "interface", }), - txMbitsNetDev: prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "livebox_interface_netdev_tx_mbits", - Help: "Transmitted Mbits per second, calculated from netdevstats.", - }, []string{ - // Name of the interface. - "interface", - }), - rxMbitsNetDev: prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "livebox_interface_netdev_rx_mbits", - Help: "Received Mbits per second, calculated from netdevstats.", - }, []string{ - // Name of the interface. - "interface", - }), - bytesSent: prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "livebox_interface_bytes_sent_total", - Help: "Bytes sent on the interface", - }, []string{ - "interface", - }), - bytesReceived: prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "livebox_interface_bytes_received_total", - Help: "Bytes received on the interface", - }, []string{ - "interface", - }), - interfaces: make(map[string]netInterface), - interfacesNetDev: make(map[string]netInterface), } } // Collectors returns all metrics. func (im *InterfaceMbits) Collectors() []prometheus.Collector { - return []prometheus.Collector{ - im.txMbits, - im.rxMbits, - im.txMbitsNetDev, - im.rxMbitsNetDev, - im.bytesSent, - im.bytesReceived, - } -} - -func bitsPer30SecsToMbitsPerSec(v int) float64 { - return float64(v) / 30000000 -} - -func (im *InterfaceMbits) discoverInterfaces(ctx context.Context) error { - var mibs struct { - Status struct { - //GPON map[string]struct{} `json:"gpon"` - Base map[string]struct { - Flags string `json:"flags"` - } `json:"base"` - } `json:"status"` - } - - if err := im.client.Request( - ctx, - request.New("NeMo.Intf.data", "getMIBs", map[string]interface{}{ - "traverse": "all", - "flag": "statmon && !vlan", - }), - &mibs, - ); err != nil { - return fmt.Errorf("failed to discover interface: %w", err) - } - - if len(mibs.Status.Base) == 0 { - return errors.New("wan interface not found") - } - - for itf, val := range mibs.Status.Base { - im.interfaces[itf] = netInterface{Flags: val.Flags} - im.interfacesNetDev[itf] = netInterface{Flags: val.Flags} - } - - return nil -} - -func bytesPerSecToMbits(bytes float64) float64 { - return bytes * 8 / 1000000 + return []prometheus.Collector{im.txMbits, im.rxMbits} } // Poll polls the current bandwidth usage. func (im *InterfaceMbits) Poll(ctx context.Context) error { - if len(im.interfaces) == 0 { - if err := im.discoverInterfaces(ctx); err != nil { - return err - } + 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"` } - eg, ctx := errgroup.WithContext(ctx) + // 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) + } - eg.Go(func() error { - return im.pollInterfaces(ctx) - }) + for iface, traffic := range counters.Status { + rxCounter := 0 + txCounter := 0 - eg.Go(func() error { - return im.pollInterfacesNetDev(ctx) - }) - - return eg.Wait() -} - -func (im *InterfaceMbits) pollInterfaces(ctx context.Context) error { - for itf, val := range im.interfaces { - elapsed := time.Now().Sub(val.LastPoll) - if elapsed.Seconds() < 30 { - // Polling must only be done once every 30 seconds Livebox updates data - // only every 30 seconds. - continue + if len(traffic.Traffic) > 0 { + rxCounter = traffic.Traffic[0].RxCounter + txCounter = traffic.Traffic[0].TxCounter } - var stats struct { - Status struct { - BytesReceived int64 `json:"BytesReceived"` - BytesSent int64 `json:"BytesSent"` - } `json:"status"` - } - - if err := im.client.Request(ctx, request.New( - fmt.Sprintf("HomeLan.Interface.%s.Stats", itf), - "get", - nil, - ), &stats); err != nil { - return err - } - - rxMetric := im.rxMbits - txMetric := im.txMbits - brMetric := im.bytesReceived - bsMetric := im.bytesSent - - if !val.IsWAN() { - rxMetric = im.txMbits - txMetric = im.rxMbits - brMetric = im.bytesSent - bsMetric = im.bytesReceived - } - - if !val.LastPoll.IsZero() { - if elapsed.Seconds() > 0 { - if stats.Status.BytesReceived >= val.LastRx { - diff := float64(stats.Status.BytesReceived - val.LastRx) - rxMetric. - With(prometheus.Labels{"interface": itf}). - Set(bytesPerSecToMbits(diff / (elapsed.Seconds()))) - brMetric.With(prometheus.Labels{"interface": itf}).Add(diff) - } else { - // Counter was reset? - brMetric.Reset() - brMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesReceived)) - } - - if stats.Status.BytesSent >= val.LastTx { - diff := float64(stats.Status.BytesSent - val.LastTx) - txMetric. - With(prometheus.Labels{"interface": itf}). - Set(bytesPerSecToMbits(diff / (elapsed.Seconds()))) - bsMetric.With(prometheus.Labels{"interface": itf}).Add(diff) - - } else { - // Counter was reset? - bsMetric.Reset() - bsMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesSent)) - } - } - } else { - // Initialize bytes - bsMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesSent)) - brMetric.With(prometheus.Labels{"interface": itf}).Add(float64(stats.Status.BytesReceived)) - } - - val.LastTx = stats.Status.BytesSent - val.LastRx = stats.Status.BytesReceived - - val.LastPoll = time.Now() - im.interfaces[itf] = val - } - - return nil -} - -func (im *InterfaceMbits) pollInterfacesNetDev(ctx context.Context) error { - for itf, val := range im.interfacesNetDev { - var stats struct { - Status struct { - RxBytes int64 `json:"RxBytes"` - TxBytes int64 `json:"TxBytes"` - } `json:"status"` - } - - if err := im.client.Request(ctx, request.New( - fmt.Sprintf("NeMo.Intf.%s", itf), - "getNetDevStats", - nil, - ), &stats); err != nil { - return err - } - - rxMetric := im.rxMbitsNetDev - txMetric := im.txMbitsNetDev - - if !val.IsWAN() { - rxMetric = im.txMbitsNetDev - txMetric = im.rxMbitsNetDev - } - - if !val.LastPoll.IsZero() { - elapsed := time.Now().Sub(val.LastPoll) - if elapsed.Seconds() > 0 { - if stats.Status.RxBytes >= val.LastRx { - rxMetric. - With(prometheus.Labels{"interface": itf}). - Set(8 * float64(stats.Status.RxBytes-val.LastRx) / (elapsed.Seconds() * 1000000)) - } - - if stats.Status.TxBytes >= val.LastTx { - txMetric. - With(prometheus.Labels{"interface": itf}). - Set(8 * float64(stats.Status.TxBytes-val.LastTx) / (elapsed.Seconds() * 1000000)) - } - - } - } - - val.LastRx = stats.Status.RxBytes - val.LastTx = stats.Status.TxBytes - val.LastPoll = time.Now() - im.interfacesNetDev[itf] = val + im.rxMbits. + With(prometheus.Labels{"interface": iface}). + Set(bitrate.BitsPer30SecsToMbits(rxCounter)) + im.txMbits. + With(prometheus.Labels{"interface": iface}). + Set(bitrate.BitsPer30SecsToMbits(txCounter)) } return nil diff --git a/internal/poller/interface_homelan.go b/internal/poller/interface_homelan.go new file mode 100644 index 0000000..666fdc6 --- /dev/null +++ b/internal/poller/interface_homelan.go @@ -0,0 +1,110 @@ +package poller + +import ( + "context" + "fmt" + "time" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" + "github.com/prometheus/client_golang/prometheus" +) + +// InterfaceHomeLanMbitsMinDelay set the minimum delay between each poll. +// Polling must only be done once every 30 seconds as Livebox updates data +// only every 30 seconds. +const InterfaceHomeLanMbitsMinDelay = 30 * time.Second + +var _ Poller = &InterfaceHomeLanMbits{} + +// InterfaceHomeLanMbits is an experimental poller to get the current bandwidth +// usage on the Livebox interfaces. +type InterfaceHomeLanMbits struct { + client livebox.Client + interfaces []*exporterLivebox.Interface + bitrate *bitrate.Bitrate + txMbits, rxMbits *prometheus.GaugeVec +} + +// NewInterfaceHomeLanMbits returns a new InterfaceMbits poller. +func NewInterfaceHomeLanMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceHomeLanMbits { + return &InterfaceHomeLanMbits{ + client: client, + interfaces: interfaces, + bitrate: bitrate.New(InterfaceHomeLanMbitsMinDelay), + txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_homelan_tx_mbits", + Help: "Transmitted Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_homelan_rx_mbits", + Help: "Received Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + } +} + +// Collectors returns all metrics. +func (im *InterfaceHomeLanMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceHomeLanMbits) Poll(ctx context.Context) error { + for _, itf := range im.interfaces { + // Enforce InterfaceHomeLanMbitsMinDelay. + if !im.bitrate.ShouldMeasure(itf.Name) { + continue + } + + var stats struct { + Status struct { + BytesReceived uint64 `json:"BytesReceived"` + BytesSent uint64 `json:"BytesSent"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("HomeLan.Interface.%s.Stats", itf.Name), + "get", + nil, + ), &stats); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.BytesSent, + Rx: stats.Status.BytesReceived, + } + + if itf.IsWAN() { + counters.Swap() + } + + bitrates := im.bitrate.Measure(itf.Name, counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Tx.Value)) + } + } + + return nil +} diff --git a/internal/poller/interface_netdev.go b/internal/poller/interface_netdev.go new file mode 100644 index 0000000..2dec0a5 --- /dev/null +++ b/internal/poller/interface_netdev.go @@ -0,0 +1,99 @@ +package poller + +import ( + "context" + "fmt" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &InterfaceNetDevMbits{} + +// InterfaceNetDevMbits is an experimental poller to get the current bandwidth +// usage on the Livebox interfaces. +type InterfaceNetDevMbits struct { + client livebox.Client + interfaces []*exporterLivebox.Interface + bitrate *bitrate.Bitrate + txMbits, rxMbits *prometheus.GaugeVec +} + +// NewInterfaceNetDevMbits returns a new InterfaceNetDevMbits poller. +func NewInterfaceNetDevMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceNetDevMbits { + return &InterfaceNetDevMbits{ + client: client, + interfaces: interfaces, + bitrate: bitrate.New(0), + txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_tx_mbits", + Help: "Transmitted Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_rx_mbits", + Help: "Received Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + } +} + +// Collectors returns all metrics. +func (im *InterfaceNetDevMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceNetDevMbits) Poll(ctx context.Context) error { + for _, itf := range im.interfaces { + var stats struct { + Status struct { + RxBytes uint64 `json:"RxBytes"` + TxBytes uint64 `json:"TxBytes"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("NeMo.Intf.%s", itf.Name), + "getNetDevStats", + nil, + ), &stats); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.TxBytes, + Rx: stats.Status.RxBytes, + } + + if itf.IsWAN() { + counters.Swap() + } + + bitrates := im.bitrate.Measure(itf.Name, counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Tx.Value)) + } + } + + return nil +} diff --git a/internal/poller/wan.go b/internal/poller/wan.go new file mode 100644 index 0000000..90089f0 --- /dev/null +++ b/internal/poller/wan.go @@ -0,0 +1,81 @@ +package poller + +import ( + "context" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &WANMbits{} + +// WANMbits is an experimental poller to get the current bandwidth usage on the +// WAN interface of the Livebox. +type WANMbits struct { + client livebox.Client + bitrate *bitrate.Bitrate + txMbits, rxMbits prometheus.Gauge +} + +// NewWANMbits returns a new WANMbits poller. +func NewWANMbits(client livebox.Client) *WANMbits { + return &WANMbits{ + client: client, + bitrate: bitrate.New(InterfaceHomeLanMbitsMinDelay), + txMbits: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "livebox_wan_tx_mbits", + Help: "Transmitted Mbits per second on the WAN interface.", + }), + rxMbits: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "livebox_wan_rx_mbits", + Help: "Received Mbits per second on the WAN interface.", + }), + } +} + +// Collectors returns all metrics. +func (im *WANMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage on the WAN interface. +func (im *WANMbits) Poll(ctx context.Context) error { + var stats struct { + Status struct { + BytesReceived uint64 `json:"BytesReceived"` + BytesSent uint64 `json:"BytesSent"` + } `json:"status"` + } + + if err := im.client.Request( + ctx, + request.New("HomeLan", "getWANCounters", nil), + &stats, + ); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.BytesSent, + Rx: stats.Status.BytesReceived, + } + + counters.Swap() + + bitrates := im.bitrate.Measure("WAN", counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits.Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits.Set(sanitizeMbits(bitrates.Tx.Value)) + } + + return nil +} diff --git a/main.go b/main.go index 5d4ad7e..fea4ac2 100644 --- a/main.go +++ b/main.go @@ -4,23 +4,104 @@ import ( "context" "errors" "flag" + "fmt" "log" "net/http" "os" + "strings" "time" "github.com/Tomy2e/livebox-api-client" "github.com/Tomy2e/livebox-exporter/internal/poller" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/exp/slices" ) -const defaultPollingFrequency = 5 +const defaultPollingFrequency = 30 + +const ( + ExperimentalMetricsInterfaceHomeLan = "livebox_interface_homelan" + ExperimentalMetricsInterfaceNetDev = "livebox_interface_netdev" + ExperimentalMetricsWAN = "livebox_wan" +) + +var experimentalMetrics = []string{ + ExperimentalMetricsInterfaceHomeLan, + ExperimentalMetricsInterfaceNetDev, + ExperimentalMetricsWAN, +} + +func parseExperimentalFlag( + ctx context.Context, + client livebox.Client, + experimental string, + pollingFrequency *uint, +) (pollers []poller.Poller) { + var ( + interfaces []*exporterLivebox.Interface + err error + + enabled = make(map[string]bool) + ) + + for _, exp := range strings.Split(experimental, ",") { + exp = strings.TrimSpace(exp) + + if !slices.Contains(experimentalMetrics, exp) { + log.Printf("WARN: Unknown experimental metrics: %s", exp) + continue + } + + if enabled[exp] { + continue + } + + // Discover interfaces for experimental pollers that require interfaces. + switch exp { + case ExperimentalMetricsInterfaceHomeLan, ExperimentalMetricsInterfaceNetDev: + if interfaces == nil { + interfaces, err = exporterLivebox.DiscoverInterfaces(ctx, client) + if err != nil { + log.Fatalf("Failed to discover Livebox interfaces: %s\n", err) + } + } + } + + switch exp { + case ExperimentalMetricsInterfaceHomeLan: + pollers = append(pollers, poller.NewInterfaceHomeLanMbits(client, interfaces)) + case ExperimentalMetricsInterfaceNetDev: + pollers = append(pollers, poller.NewInterfaceNetDevMbits(client, interfaces)) + + if *pollingFrequency > 5 { + log.Printf( + "WARN: The %s experimental metrics require a lower polling frequency, "+ + "setting polling frequency to 5 seconds\n", + ExperimentalMetricsInterfaceNetDev, + ) + *pollingFrequency = 5 + } + case ExperimentalMetricsWAN: + pollers = append(pollers, poller.NewWANMbits(client)) + } + + log.Printf("INFO: enabled experimental metrics: %s\n", exp) + enabled[exp] = true + } + + return +} func main() { pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency") listen := flag.String("listen", ":8080", "Listening address") + experimental := flag.String("experimental", "", fmt.Sprintf( + "Comma separated list of experimental metrics to enable (available metrics: %s)", + strings.Join(experimentalMetrics, ","), + )) flag.Parse() adminPassword := os.Getenv("ADMIN_PASSWORD") @@ -38,6 +119,9 @@ func main() { } ) + // Add experimental pollers. + pollers = append(pollers, parseExperimentalFlag(ctx, client, *experimental, pollingFrequency)...) + registry.MustRegister( append( pollers.Collectors(), diff --git a/pkg/bitrate/bitrate.go b/pkg/bitrate/bitrate.go new file mode 100644 index 0000000..35b4a60 --- /dev/null +++ b/pkg/bitrate/bitrate.go @@ -0,0 +1,106 @@ +package bitrate + +import ( + "time" +) + +// Bitrate allows calculating bitrates for a set of network interfaces. +// This implementation is not thread-safe. +type Bitrate struct { + measures map[string]*measure + minDelayBetweenMeasures time.Duration +} + +// New returns a new bitrate measurer. +func New(minDelayBetweenMeasures time.Duration) *Bitrate { + return &Bitrate{ + measures: make(map[string]*measure), + minDelayBetweenMeasures: minDelayBetweenMeasures, + } +} + +// mesure saves the counter values at a specific point in time. +type measure struct { + Counters + Last time.Time +} + +// Counters contain Tx and Rx counters for a network interface. +type Counters struct { + Tx, Rx uint64 +} + +// Swap swaps Tx and Rx counters. +func (c *Counters) Swap() { + c.Rx, c.Tx = c.Tx, c.Rx +} + +// Bitrates for Tx and Rx. +type Bitrates struct { + // Tx bitrate, can be nil if not available. + Tx *BitrateSpec + // Rx bitrate, can be nil if not available. + Rx *BitrateSpec +} + +// BitrateSpec contains the value of the bitrate +type BitrateSpec struct { + // Value of the bitrate (in Mbit/s). Will be 0 if Reset is true. + Value float64 + // Reset is true when the counter was reset. + Reset bool +} + +// ShouldMeasure returns true if a measure should be done. +func (b *Bitrate) ShouldMeasure(name string) bool { + last, ok := b.measures[name] + if !ok { + return true + } + + return time.Now().Sub(last.Last) > b.minDelayBetweenMeasures +} + +// Measure saves the current measure and returns the current RX/TX bitrates. +func (b *Bitrate) Measure(name string, current *Counters) *Bitrates { + br := &Bitrates{} + + last, ok := b.measures[name] + + // Only calculate bitrates if there is a previous measure. + if ok && !last.Last.IsZero() { + elapsed := time.Now().Sub(last.Last) + + if elapsed.Seconds() > 0 { + diff := current.Rx - last.Rx + if diff >= 0 { + br.Rx = &BitrateSpec{ + Value: BytesPerSecToMbits(float64(diff) / elapsed.Seconds()), + } + } else { + br.Rx = &BitrateSpec{ + Reset: true, + } + } + + diff = current.Tx - last.Tx + if diff >= 0 { + br.Tx = &BitrateSpec{ + Value: BytesPerSecToMbits(float64(diff) / elapsed.Seconds()), + } + } else { + br.Tx = &BitrateSpec{ + Reset: true, + } + } + } + } + + // Save this measure as the latest. + b.measures[name] = &measure{ + Counters: *current, + Last: time.Now(), + } + + return br +} diff --git a/pkg/bitrate/conversion.go b/pkg/bitrate/conversion.go new file mode 100644 index 0000000..243c488 --- /dev/null +++ b/pkg/bitrate/conversion.go @@ -0,0 +1,11 @@ +package bitrate + +// BitsPer30SecsToMbits converts bits/30secs to Mbit/s. +func BitsPer30SecsToMbits(v int) float64 { + return float64(v) / 30000000 +} + +// BytesPerSecToMbits converts B/s to Mbit/s. +func BytesPerSecToMbits(bytes float64) float64 { + return bytes * 8 / 1000000 +} diff --git a/pkg/livebox/discovery.go b/pkg/livebox/discovery.go new file mode 100644 index 0000000..6e496f7 --- /dev/null +++ b/pkg/livebox/discovery.go @@ -0,0 +1,64 @@ +package livebox + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" +) + +// Interface is a Livebox Network interface. +type Interface struct { + Name string + Flags string +} + +// IsWAN returns true if this interface is a WAN interface. +func (i *Interface) IsWAN() bool { + return strings.Contains(i.Flags, "wan") +} + +// IsWLAN returns true if this interface is a WLAN interface. +func (i *Interface) IsWLAN() bool { + return strings.Contains(i.Flags, "wlanvap") +} + +// DiscoverInterfaces discovers network interfaces on the Livebox. +func DiscoverInterfaces(ctx context.Context, client livebox.Client) ([]*Interface, error) { + var mibs struct { + Status struct { + Base map[string]struct { + Flags string `json:"flags"` + } `json:"base"` + } `json:"status"` + } + + if err := client.Request( + ctx, + request.New("NeMo.Intf.data", "getMIBs", map[string]interface{}{ + "traverse": "all", + "flag": "statmon && !vlan", + }), + &mibs, + ); err != nil { + return nil, fmt.Errorf("failed to discover interfaces: %w", err) + } + + if len(mibs.Status.Base) == 0 { + return nil, errors.New("no interfaces found") + } + + itfs := make([]*Interface, 0, len(mibs.Status.Base)) + + for itf, val := range mibs.Status.Base { + itfs = append(itfs, &Interface{ + Name: itf, + Flags: val.Flags, + }) + } + + return itfs, nil +} From acd5e5ebfe22594014971db709371788b406fbc2 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Sat, 18 Mar 2023 19:42:27 +0100 Subject: [PATCH 3/8] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 71df353..2494f96 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ This exporter currently exposes the following metrics: | livebox_interface_homelan_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | | livebox_interface_netdev_rx_mbits | gauge | Received Mbits per second | interface | Yes | | livebox_interface_netdev_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | -| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | interface | Yes | -| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | interface | Yes | +| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | | Yes | +| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | | Yes | Experimental metrics are not enabled by default, use the `-experimental` command-line option to enable them. From 2da35e87726fccef20fea365547fd757544fd27f Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Wed, 5 Apr 2023 19:23:43 +0200 Subject: [PATCH 4/8] reverse counters swap logic --- internal/poller/interface_homelan.go | 2 +- internal/poller/interface_netdev.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/poller/interface_homelan.go b/internal/poller/interface_homelan.go index 666fdc6..02e882b 100644 --- a/internal/poller/interface_homelan.go +++ b/internal/poller/interface_homelan.go @@ -87,7 +87,7 @@ func (im *InterfaceHomeLanMbits) Poll(ctx context.Context) error { Rx: stats.Status.BytesReceived, } - if itf.IsWAN() { + if !itf.IsWAN() { counters.Swap() } diff --git a/internal/poller/interface_netdev.go b/internal/poller/interface_netdev.go index 2dec0a5..306e91f 100644 --- a/internal/poller/interface_netdev.go +++ b/internal/poller/interface_netdev.go @@ -76,7 +76,7 @@ func (im *InterfaceNetDevMbits) Poll(ctx context.Context) error { Rx: stats.Status.RxBytes, } - if itf.IsWAN() { + if !itf.IsWAN() { counters.Swap() } From 136f95a9b78c52d2411b8631ea14a30fad7c5e72 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Fri, 21 Apr 2023 18:58:34 +0200 Subject: [PATCH 5/8] Add new experimental metrics (#4) --- README.md | 28 ++++--- go.mod | 3 +- go.sum | 6 +- internal/poller/common.go | 9 +++ internal/poller/interface.go | 9 +-- internal/poller/interface_homelan.go | 110 +++++++++++++++++++++++++++ internal/poller/interface_netdev.go | 99 ++++++++++++++++++++++++ internal/poller/wan.go | 81 ++++++++++++++++++++ main.go | 84 ++++++++++++++++++++ pkg/bitrate/bitrate.go | 106 ++++++++++++++++++++++++++ pkg/bitrate/conversion.go | 11 +++ pkg/livebox/discovery.go | 64 ++++++++++++++++ 12 files changed, 592 insertions(+), 18 deletions(-) create mode 100644 internal/poller/common.go create mode 100644 internal/poller/interface_homelan.go create mode 100644 internal/poller/interface_netdev.go create mode 100644 internal/poller/wan.go create mode 100644 pkg/bitrate/bitrate.go create mode 100644 pkg/bitrate/conversion.go create mode 100644 pkg/livebox/discovery.go diff --git a/README.md b/README.md index 649556e..2494f96 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,20 @@ 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 | -| livebox_devices_total | gauge | The total number of active devices | type | +| Name | Type | Description | Labels | Experimental | +| ---------------------------------- | ----- | ------------------------------------------------- | --------- | ------------ | +| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | No | +| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | No | +| livebox_devices_total | gauge | The total number of active devices | type | No | +| livebox_interface_homelan_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_homelan_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_interface_netdev_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_netdev_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | | Yes | +| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | | Yes | + +Experimental metrics are not enabled by default, use the `-experimental` +command-line option to enable them. ## Usage @@ -19,10 +28,11 @@ This exporter currently exposes the following metrics: The exporter accepts the following command-line options: -| Name | Description | Default value | -| ------------------- | ----------------- | ------------- | -| --polling-frequency | Polling frequency | 30 | -| --listen | Listening address | :8080 | +| Name | Description | Default value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| -polling-frequency | Polling frequency | 30 | +| -listen | Listening address | :8080 | +| -experimental | Comma separated list of experimental metrics to enable (available metrics: livebox_interface_homelan,livebox_interface_netdev,livebox_wan) | | The exporter reads the following environment variables: diff --git a/go.mod b/go.mod index 7e83d4f..f497aca 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7 github.com/prometheus/client_golang v1.14.0 + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 golang.org/x/sync v0.1.0 ) @@ -16,6 +17,6 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/sys v0.1.0 // indirect google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 1a55841..acacb92 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -325,8 +327,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/poller/common.go b/internal/poller/common.go new file mode 100644 index 0000000..b4f004b --- /dev/null +++ b/internal/poller/common.go @@ -0,0 +1,9 @@ +package poller + +import "math" + +const maxMbits = 2150 + +func sanitizeMbits(mbits float64) float64 { + return math.Min(mbits, maxMbits) +} diff --git a/internal/poller/interface.go b/internal/poller/interface.go index 2140891..03c152e 100644 --- a/internal/poller/interface.go +++ b/internal/poller/interface.go @@ -6,6 +6,7 @@ import ( "github.com/Tomy2e/livebox-api-client" "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" "github.com/prometheus/client_golang/prometheus" ) @@ -44,10 +45,6 @@ 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 { @@ -87,10 +84,10 @@ func (im *InterfaceMbits) Poll(ctx context.Context) error { im.rxMbits. With(prometheus.Labels{"interface": iface}). - Set(bitsPer30SecsToMbitsPerSec(rxCounter)) + Set(bitrate.BitsPer30SecsToMbits(rxCounter)) im.txMbits. With(prometheus.Labels{"interface": iface}). - Set(bitsPer30SecsToMbitsPerSec(txCounter)) + Set(bitrate.BitsPer30SecsToMbits(txCounter)) } return nil diff --git a/internal/poller/interface_homelan.go b/internal/poller/interface_homelan.go new file mode 100644 index 0000000..02e882b --- /dev/null +++ b/internal/poller/interface_homelan.go @@ -0,0 +1,110 @@ +package poller + +import ( + "context" + "fmt" + "time" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" + "github.com/prometheus/client_golang/prometheus" +) + +// InterfaceHomeLanMbitsMinDelay set the minimum delay between each poll. +// Polling must only be done once every 30 seconds as Livebox updates data +// only every 30 seconds. +const InterfaceHomeLanMbitsMinDelay = 30 * time.Second + +var _ Poller = &InterfaceHomeLanMbits{} + +// InterfaceHomeLanMbits is an experimental poller to get the current bandwidth +// usage on the Livebox interfaces. +type InterfaceHomeLanMbits struct { + client livebox.Client + interfaces []*exporterLivebox.Interface + bitrate *bitrate.Bitrate + txMbits, rxMbits *prometheus.GaugeVec +} + +// NewInterfaceHomeLanMbits returns a new InterfaceMbits poller. +func NewInterfaceHomeLanMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceHomeLanMbits { + return &InterfaceHomeLanMbits{ + client: client, + interfaces: interfaces, + bitrate: bitrate.New(InterfaceHomeLanMbitsMinDelay), + txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_homelan_tx_mbits", + Help: "Transmitted Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_homelan_rx_mbits", + Help: "Received Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + } +} + +// Collectors returns all metrics. +func (im *InterfaceHomeLanMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceHomeLanMbits) Poll(ctx context.Context) error { + for _, itf := range im.interfaces { + // Enforce InterfaceHomeLanMbitsMinDelay. + if !im.bitrate.ShouldMeasure(itf.Name) { + continue + } + + var stats struct { + Status struct { + BytesReceived uint64 `json:"BytesReceived"` + BytesSent uint64 `json:"BytesSent"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("HomeLan.Interface.%s.Stats", itf.Name), + "get", + nil, + ), &stats); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.BytesSent, + Rx: stats.Status.BytesReceived, + } + + if !itf.IsWAN() { + counters.Swap() + } + + bitrates := im.bitrate.Measure(itf.Name, counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Tx.Value)) + } + } + + return nil +} diff --git a/internal/poller/interface_netdev.go b/internal/poller/interface_netdev.go new file mode 100644 index 0000000..306e91f --- /dev/null +++ b/internal/poller/interface_netdev.go @@ -0,0 +1,99 @@ +package poller + +import ( + "context" + "fmt" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &InterfaceNetDevMbits{} + +// InterfaceNetDevMbits is an experimental poller to get the current bandwidth +// usage on the Livebox interfaces. +type InterfaceNetDevMbits struct { + client livebox.Client + interfaces []*exporterLivebox.Interface + bitrate *bitrate.Bitrate + txMbits, rxMbits *prometheus.GaugeVec +} + +// NewInterfaceNetDevMbits returns a new InterfaceNetDevMbits poller. +func NewInterfaceNetDevMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceNetDevMbits { + return &InterfaceNetDevMbits{ + client: client, + interfaces: interfaces, + bitrate: bitrate.New(0), + txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_tx_mbits", + Help: "Transmitted Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + rxMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "livebox_interface_netdev_rx_mbits", + Help: "Received Mbits per second.", + }, []string{ + // Name of the interface. + "interface", + }), + } +} + +// Collectors returns all metrics. +func (im *InterfaceNetDevMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage. +func (im *InterfaceNetDevMbits) Poll(ctx context.Context) error { + for _, itf := range im.interfaces { + var stats struct { + Status struct { + RxBytes uint64 `json:"RxBytes"` + TxBytes uint64 `json:"TxBytes"` + } `json:"status"` + } + + if err := im.client.Request(ctx, request.New( + fmt.Sprintf("NeMo.Intf.%s", itf.Name), + "getNetDevStats", + nil, + ), &stats); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.TxBytes, + Rx: stats.Status.RxBytes, + } + + if !itf.IsWAN() { + counters.Swap() + } + + bitrates := im.bitrate.Measure(itf.Name, counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits. + With(prometheus.Labels{"interface": itf.Name}). + Set(sanitizeMbits(bitrates.Tx.Value)) + } + } + + return nil +} diff --git a/internal/poller/wan.go b/internal/poller/wan.go new file mode 100644 index 0000000..90089f0 --- /dev/null +++ b/internal/poller/wan.go @@ -0,0 +1,81 @@ +package poller + +import ( + "context" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/Tomy2e/livebox-exporter/pkg/bitrate" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Poller = &WANMbits{} + +// WANMbits is an experimental poller to get the current bandwidth usage on the +// WAN interface of the Livebox. +type WANMbits struct { + client livebox.Client + bitrate *bitrate.Bitrate + txMbits, rxMbits prometheus.Gauge +} + +// NewWANMbits returns a new WANMbits poller. +func NewWANMbits(client livebox.Client) *WANMbits { + return &WANMbits{ + client: client, + bitrate: bitrate.New(InterfaceHomeLanMbitsMinDelay), + txMbits: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "livebox_wan_tx_mbits", + Help: "Transmitted Mbits per second on the WAN interface.", + }), + rxMbits: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "livebox_wan_rx_mbits", + Help: "Received Mbits per second on the WAN interface.", + }), + } +} + +// Collectors returns all metrics. +func (im *WANMbits) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + im.txMbits, + im.rxMbits, + } +} + +// Poll polls the current bandwidth usage on the WAN interface. +func (im *WANMbits) Poll(ctx context.Context) error { + var stats struct { + Status struct { + BytesReceived uint64 `json:"BytesReceived"` + BytesSent uint64 `json:"BytesSent"` + } `json:"status"` + } + + if err := im.client.Request( + ctx, + request.New("HomeLan", "getWANCounters", nil), + &stats, + ); err != nil { + return err + } + + counters := &bitrate.Counters{ + Tx: stats.Status.BytesSent, + Rx: stats.Status.BytesReceived, + } + + counters.Swap() + + bitrates := im.bitrate.Measure("WAN", counters) + + if bitrates.Rx != nil && !bitrates.Rx.Reset { + im.rxMbits.Set(sanitizeMbits(bitrates.Rx.Value)) + } + + if bitrates.Tx != nil && !bitrates.Tx.Reset { + im.txMbits.Set(sanitizeMbits(bitrates.Tx.Value)) + } + + return nil +} diff --git a/main.go b/main.go index 3f67a1e..fea4ac2 100644 --- a/main.go +++ b/main.go @@ -4,23 +4,104 @@ import ( "context" "errors" "flag" + "fmt" "log" "net/http" "os" + "strings" "time" "github.com/Tomy2e/livebox-api-client" "github.com/Tomy2e/livebox-exporter/internal/poller" + exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/exp/slices" ) const defaultPollingFrequency = 30 +const ( + ExperimentalMetricsInterfaceHomeLan = "livebox_interface_homelan" + ExperimentalMetricsInterfaceNetDev = "livebox_interface_netdev" + ExperimentalMetricsWAN = "livebox_wan" +) + +var experimentalMetrics = []string{ + ExperimentalMetricsInterfaceHomeLan, + ExperimentalMetricsInterfaceNetDev, + ExperimentalMetricsWAN, +} + +func parseExperimentalFlag( + ctx context.Context, + client livebox.Client, + experimental string, + pollingFrequency *uint, +) (pollers []poller.Poller) { + var ( + interfaces []*exporterLivebox.Interface + err error + + enabled = make(map[string]bool) + ) + + for _, exp := range strings.Split(experimental, ",") { + exp = strings.TrimSpace(exp) + + if !slices.Contains(experimentalMetrics, exp) { + log.Printf("WARN: Unknown experimental metrics: %s", exp) + continue + } + + if enabled[exp] { + continue + } + + // Discover interfaces for experimental pollers that require interfaces. + switch exp { + case ExperimentalMetricsInterfaceHomeLan, ExperimentalMetricsInterfaceNetDev: + if interfaces == nil { + interfaces, err = exporterLivebox.DiscoverInterfaces(ctx, client) + if err != nil { + log.Fatalf("Failed to discover Livebox interfaces: %s\n", err) + } + } + } + + switch exp { + case ExperimentalMetricsInterfaceHomeLan: + pollers = append(pollers, poller.NewInterfaceHomeLanMbits(client, interfaces)) + case ExperimentalMetricsInterfaceNetDev: + pollers = append(pollers, poller.NewInterfaceNetDevMbits(client, interfaces)) + + if *pollingFrequency > 5 { + log.Printf( + "WARN: The %s experimental metrics require a lower polling frequency, "+ + "setting polling frequency to 5 seconds\n", + ExperimentalMetricsInterfaceNetDev, + ) + *pollingFrequency = 5 + } + case ExperimentalMetricsWAN: + pollers = append(pollers, poller.NewWANMbits(client)) + } + + log.Printf("INFO: enabled experimental metrics: %s\n", exp) + enabled[exp] = true + } + + return +} + func main() { pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency") listen := flag.String("listen", ":8080", "Listening address") + experimental := flag.String("experimental", "", fmt.Sprintf( + "Comma separated list of experimental metrics to enable (available metrics: %s)", + strings.Join(experimentalMetrics, ","), + )) flag.Parse() adminPassword := os.Getenv("ADMIN_PASSWORD") @@ -38,6 +119,9 @@ func main() { } ) + // Add experimental pollers. + pollers = append(pollers, parseExperimentalFlag(ctx, client, *experimental, pollingFrequency)...) + registry.MustRegister( append( pollers.Collectors(), diff --git a/pkg/bitrate/bitrate.go b/pkg/bitrate/bitrate.go new file mode 100644 index 0000000..35b4a60 --- /dev/null +++ b/pkg/bitrate/bitrate.go @@ -0,0 +1,106 @@ +package bitrate + +import ( + "time" +) + +// Bitrate allows calculating bitrates for a set of network interfaces. +// This implementation is not thread-safe. +type Bitrate struct { + measures map[string]*measure + minDelayBetweenMeasures time.Duration +} + +// New returns a new bitrate measurer. +func New(minDelayBetweenMeasures time.Duration) *Bitrate { + return &Bitrate{ + measures: make(map[string]*measure), + minDelayBetweenMeasures: minDelayBetweenMeasures, + } +} + +// mesure saves the counter values at a specific point in time. +type measure struct { + Counters + Last time.Time +} + +// Counters contain Tx and Rx counters for a network interface. +type Counters struct { + Tx, Rx uint64 +} + +// Swap swaps Tx and Rx counters. +func (c *Counters) Swap() { + c.Rx, c.Tx = c.Tx, c.Rx +} + +// Bitrates for Tx and Rx. +type Bitrates struct { + // Tx bitrate, can be nil if not available. + Tx *BitrateSpec + // Rx bitrate, can be nil if not available. + Rx *BitrateSpec +} + +// BitrateSpec contains the value of the bitrate +type BitrateSpec struct { + // Value of the bitrate (in Mbit/s). Will be 0 if Reset is true. + Value float64 + // Reset is true when the counter was reset. + Reset bool +} + +// ShouldMeasure returns true if a measure should be done. +func (b *Bitrate) ShouldMeasure(name string) bool { + last, ok := b.measures[name] + if !ok { + return true + } + + return time.Now().Sub(last.Last) > b.minDelayBetweenMeasures +} + +// Measure saves the current measure and returns the current RX/TX bitrates. +func (b *Bitrate) Measure(name string, current *Counters) *Bitrates { + br := &Bitrates{} + + last, ok := b.measures[name] + + // Only calculate bitrates if there is a previous measure. + if ok && !last.Last.IsZero() { + elapsed := time.Now().Sub(last.Last) + + if elapsed.Seconds() > 0 { + diff := current.Rx - last.Rx + if diff >= 0 { + br.Rx = &BitrateSpec{ + Value: BytesPerSecToMbits(float64(diff) / elapsed.Seconds()), + } + } else { + br.Rx = &BitrateSpec{ + Reset: true, + } + } + + diff = current.Tx - last.Tx + if diff >= 0 { + br.Tx = &BitrateSpec{ + Value: BytesPerSecToMbits(float64(diff) / elapsed.Seconds()), + } + } else { + br.Tx = &BitrateSpec{ + Reset: true, + } + } + } + } + + // Save this measure as the latest. + b.measures[name] = &measure{ + Counters: *current, + Last: time.Now(), + } + + return br +} diff --git a/pkg/bitrate/conversion.go b/pkg/bitrate/conversion.go new file mode 100644 index 0000000..243c488 --- /dev/null +++ b/pkg/bitrate/conversion.go @@ -0,0 +1,11 @@ +package bitrate + +// BitsPer30SecsToMbits converts bits/30secs to Mbit/s. +func BitsPer30SecsToMbits(v int) float64 { + return float64(v) / 30000000 +} + +// BytesPerSecToMbits converts B/s to Mbit/s. +func BytesPerSecToMbits(bytes float64) float64 { + return bytes * 8 / 1000000 +} diff --git a/pkg/livebox/discovery.go b/pkg/livebox/discovery.go new file mode 100644 index 0000000..6e496f7 --- /dev/null +++ b/pkg/livebox/discovery.go @@ -0,0 +1,64 @@ +package livebox + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" +) + +// Interface is a Livebox Network interface. +type Interface struct { + Name string + Flags string +} + +// IsWAN returns true if this interface is a WAN interface. +func (i *Interface) IsWAN() bool { + return strings.Contains(i.Flags, "wan") +} + +// IsWLAN returns true if this interface is a WLAN interface. +func (i *Interface) IsWLAN() bool { + return strings.Contains(i.Flags, "wlanvap") +} + +// DiscoverInterfaces discovers network interfaces on the Livebox. +func DiscoverInterfaces(ctx context.Context, client livebox.Client) ([]*Interface, error) { + var mibs struct { + Status struct { + Base map[string]struct { + Flags string `json:"flags"` + } `json:"base"` + } `json:"status"` + } + + if err := client.Request( + ctx, + request.New("NeMo.Intf.data", "getMIBs", map[string]interface{}{ + "traverse": "all", + "flag": "statmon && !vlan", + }), + &mibs, + ); err != nil { + return nil, fmt.Errorf("failed to discover interfaces: %w", err) + } + + if len(mibs.Status.Base) == 0 { + return nil, errors.New("no interfaces found") + } + + itfs := make([]*Interface, 0, len(mibs.Status.Base)) + + for itf, val := range mibs.Status.Base { + itfs = append(itfs, &Interface{ + Name: itf, + Flags: val.Flags, + }) + } + + return itfs, nil +} From e348d3871bbb68ec81f5b23d5574cbefecd7f019 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Fri, 21 Apr 2023 19:05:52 +0200 Subject: [PATCH 6/8] Bump Helm chart --- charts/livebox-exporter/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/livebox-exporter/Chart.yaml b/charts/livebox-exporter/Chart.yaml index b3eb611..171c10a 100644 --- a/charts/livebox-exporter/Chart.yaml +++ b/charts/livebox-exporter/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: livebox-exporter description: A prometheus exporter for Livebox type: application -version: 0.1.0 -appVersion: "v0.1.0" +version: 0.2.0 +appVersion: "v0.2.0" From 5abe206e2794130fbb66146f1b6317e615c30053 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Tue, 23 May 2023 20:55:05 +0200 Subject: [PATCH 7/8] Add deviceinfo metrics (#5) --- README.md | 26 ++++---- internal/collector/deviceinfo.go | 101 +++++++++++++++++++++++++++++++ main.go | 3 + 3 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 internal/collector/deviceinfo.go diff --git a/README.md b/README.md index 2494f96..8464fd9 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,21 @@ FTTH subscription. This exporter currently exposes the following metrics: -| Name | Type | Description | Labels | Experimental | -| ---------------------------------- | ----- | ------------------------------------------------- | --------- | ------------ | -| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | No | -| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | No | -| livebox_devices_total | gauge | The total number of active devices | type | No | -| livebox_interface_homelan_rx_mbits | gauge | Received Mbits per second | interface | Yes | -| livebox_interface_homelan_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | -| livebox_interface_netdev_rx_mbits | gauge | Received Mbits per second | interface | Yes | -| livebox_interface_netdev_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | -| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | | Yes | -| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | | Yes | +| Name | Type | Description | Labels | Experimental | +| --------------------------------------- | ----- | ------------------------------------------------- | --------- | ------------ | +| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | No | +| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | No | +| livebox_devices_total | gauge | The total number of active devices | type | No | +| livebox_deviceinfo_reboots_total | gauge | Number of Livebox reboots | | No | +| livebox_deviceinfo_uptime_seconds_total | gauge | Livebox current uptime | | No | +| livebox_deviceinfo_memory_total_bytes | gauge | Livebox system total memory | | No | +| livebox_deviceinfo_memory_usage_bytes | gauge | Livebox system used memory | | No | +| livebox_interface_homelan_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_homelan_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_interface_netdev_rx_mbits | gauge | Received Mbits per second | interface | Yes | +| livebox_interface_netdev_tx_mbits | gauge | Transmitted Mbits per second | interface | Yes | +| livebox_wan_rx_mbits | gauge | Received Mbits per second on the WAN interface | | Yes | +| livebox_wan_tx_mbits | gauge | Transmitted Mbits per second on the WAN interface | | Yes | Experimental metrics are not enabled by default, use the `-experimental` command-line option to enable them. diff --git a/internal/collector/deviceinfo.go b/internal/collector/deviceinfo.go new file mode 100644 index 0000000..e317cfc --- /dev/null +++ b/internal/collector/deviceinfo.go @@ -0,0 +1,101 @@ +package collector + +import ( + "context" + "log" + "sync" + + "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-api-client/api/request" + "github.com/prometheus/client_golang/prometheus" +) + +var _ prometheus.Collector = &DeviceInfo{} + +// DeviceInfo implements a prometheus Collector that returns Livebox specific metrics. +type DeviceInfo struct { + client livebox.Client + numberOfRebootsMetric *prometheus.Desc + uptimeMetric *prometheus.Desc + memoryTotalMetric *prometheus.Desc + memoryUsageMetric *prometheus.Desc +} + +// NewDeviceInfo returns a new DeviceInfo collector using the specified client. +func NewDeviceInfo(client livebox.Client) *DeviceInfo { + return &DeviceInfo{ + client: client, + numberOfRebootsMetric: prometheus.NewDesc( + "livebox_deviceinfo_reboots_total", + "Number of Livebox reboots.", + nil, nil, + ), + uptimeMetric: prometheus.NewDesc( + "livebox_deviceinfo_uptime_seconds_total", + "Livebox current uptime.", + nil, nil, + ), + memoryTotalMetric: prometheus.NewDesc( + "livebox_deviceinfo_memory_total_bytes", + "Livebox system total memory.", + nil, nil, + ), + memoryUsageMetric: prometheus.NewDesc( + "livebox_deviceinfo_memory_usage_bytes", + "Livebox system used memory.", + nil, nil, + ), + } +} + +// Describe currently does nothing. +func (d *DeviceInfo) Describe(c chan<- *prometheus.Desc) {} + +func (d *DeviceInfo) deviceInfo(c chan<- prometheus.Metric) { + var deviceInfo struct { + Status struct { + NumberOfReboots float64 `json:"NumberOfReboots"` + UpTime float64 `json:"UpTime"` + } `json:"status"` + } + if err := d.client.Request(context.TODO(), request.New("DeviceInfo", "get", nil), &deviceInfo); err != nil { + log.Printf("WARN: DeviceInfo collector failed: %s", err) + return + } + + c <- prometheus.MustNewConstMetric(d.numberOfRebootsMetric, prometheus.GaugeValue, deviceInfo.Status.NumberOfReboots) + c <- prometheus.MustNewConstMetric(d.uptimeMetric, prometheus.GaugeValue, deviceInfo.Status.UpTime) +} + +func (d *DeviceInfo) memoryStatus(c chan<- prometheus.Metric) { + var memoryStatus struct { + Status struct { + Total float64 `json:"Total"` + Free float64 `json:"Free"` + } `json:"status"` + } + if err := d.client.Request(context.TODO(), request.New("DeviceInfo.MemoryStatus", "get", nil), &memoryStatus); err != nil { + log.Printf("WARN: MemoryStatus collector failed: %s", err) + return + } + + c <- prometheus.MustNewConstMetric(d.memoryTotalMetric, prometheus.GaugeValue, 1000*memoryStatus.Status.Total) + c <- prometheus.MustNewConstMetric(d.memoryUsageMetric, prometheus.GaugeValue, 1000*(memoryStatus.Status.Total-memoryStatus.Status.Free)) +} + +// Collect collects all DeviceInfo metrics. +func (d *DeviceInfo) Collect(c chan<- prometheus.Metric) { + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + d.deviceInfo(c) + wg.Done() + }() + + wg.Add(1) + go func() { + d.memoryStatus(c) + wg.Done() + }() + wg.Wait() +} diff --git a/main.go b/main.go index fea4ac2..fd4b671 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/Tomy2e/livebox-api-client" + "github.com/Tomy2e/livebox-exporter/internal/collector" "github.com/Tomy2e/livebox-exporter/internal/poller" exporterLivebox "github.com/Tomy2e/livebox-exporter/pkg/livebox" "github.com/prometheus/client_golang/prometheus" @@ -130,6 +131,8 @@ func main() { )..., ) + registry.MustRegister(collector.NewDeviceInfo(client)) + go func() { for { if err := pollers.Poll(ctx); err != nil { From 4a5496d239e0525686eae0d54f86dd0158140057 Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Sat, 3 Jun 2023 11:30:01 +0200 Subject: [PATCH 8/8] Allow using a custom livebox address (#7) --- README.md | 8 +- charts/livebox-exporter/Chart.yaml | 4 +- .../livebox-exporter/templates/configmap.yaml | 10 +++ .../templates/deployment.yaml | 25 ++++++- charts/livebox-exporter/templates/secret.yaml | 4 +- charts/livebox-exporter/values.yaml | 21 +++--- go.mod | 2 +- go.sum | 4 +- internal/collector/deviceinfo.go | 4 +- internal/poller/devices.go | 4 +- internal/poller/interface.go | 4 +- internal/poller/interface_homelan.go | 4 +- internal/poller/interface_netdev.go | 4 +- internal/poller/wan.go | 4 +- main.go | 75 ++++++++++++++++++- pkg/livebox/discovery.go | 2 +- 16 files changed, 141 insertions(+), 38 deletions(-) create mode 100644 charts/livebox-exporter/templates/configmap.yaml diff --git a/README.md b/README.md index 8464fd9..fbd7a39 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,11 @@ The exporter accepts the following command-line options: The exporter reads the following environment variables: -| Name | Description | Default value | -| -------------- | --------------------------------------------------------------------------------------------------------- | ------------- | -| ADMIN_PASSWORD | Password of the Livebox "admin" user. The exporter will exit if this environment variable is not defined. | | +| Name | Description | Default value | +| --------------- | --------------------------------------------------------------------------------------------------------- | -------------------- | +| ADMIN_PASSWORD | Password of the Livebox "admin" user. The exporter will exit if this environment variable is not defined. | | +| LIVEBOX_ADDRESS | Address of the Livebox. | `http://192.168.1.1` | +| LIVEBOX_CACERT | Optional path to a PEM-encoded CA certificate file on the local disk. | | ### Docker diff --git a/charts/livebox-exporter/Chart.yaml b/charts/livebox-exporter/Chart.yaml index 171c10a..fe972e1 100644 --- a/charts/livebox-exporter/Chart.yaml +++ b/charts/livebox-exporter/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: livebox-exporter description: A prometheus exporter for Livebox type: application -version: 0.2.0 -appVersion: "v0.2.0" +version: 0.3.0 +appVersion: "v0.3.0" diff --git a/charts/livebox-exporter/templates/configmap.yaml b/charts/livebox-exporter/templates/configmap.yaml new file mode 100644 index 0000000..aefe3a2 --- /dev/null +++ b/charts/livebox-exporter/templates/configmap.yaml @@ -0,0 +1,10 @@ +{{- if .Values.livebox.caCert }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "livebox-exporter.fullname" . }} + labels: + {{- include "livebox-exporter.labels" . | nindent 4 }} +data: + ca.crt: {{ toYaml .Values.livebox.caCert | indent 2 }} +{{- end }} \ No newline at end of file diff --git a/charts/livebox-exporter/templates/deployment.yaml b/charts/livebox-exporter/templates/deployment.yaml index be2aa8d..e7cb588 100644 --- a/charts/livebox-exporter/templates/deployment.yaml +++ b/charts/livebox-exporter/templates/deployment.yaml @@ -41,13 +41,26 @@ spec: - name: ADMIN_PASSWORD valueFrom: secretKeyRef: - {{- if .Values.adminPassword.secretKeyRef }} - name: {{ .Values.adminPassword.secretKeyRef.name}} - key: {{ .Values.adminPassword.secretKeyRef.key }} + {{- if .Values.livebox.adminPassword.secretKeyRef }} + name: {{ .Values.livebox.adminPassword.secretKeyRef.name}} + key: {{ .Values.livebox.adminPassword.secretKeyRef.key }} {{- else }} name: {{ include "livebox-exporter.fullname" . }}-admin key: password {{- end }} + {{- with .Values.livebox.address }} + - name: LIVEBOX_ADDRESS + value: {{ . }} + {{- end }} + {{- if .Values.livebox.caCert }} + - name: LIVEBOX_CACERT + value: /etc/livebox/certs/ca.crt + {{- end }} + {{- if .Values.livebox.caCert }} + volumeMounts: + - name: livebox-crt + mountPath: /etc/livebox/certs + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -60,3 +73,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.livebox.caCert }} + volumes: + - name: livebox-crt + configMap: + name: {{ include "livebox-exporter.fullname" . }} + {{- end }} \ No newline at end of file diff --git a/charts/livebox-exporter/templates/secret.yaml b/charts/livebox-exporter/templates/secret.yaml index f2f0c3b..18a30f7 100644 --- a/charts/livebox-exporter/templates/secret.yaml +++ b/charts/livebox-exporter/templates/secret.yaml @@ -1,4 +1,4 @@ -{{- if not .Values.adminPassword.secretKeyRef }} +{{- if not .Values.livebox.adminPassword.secretKeyRef }} apiVersion: v1 kind: Secret metadata: @@ -7,5 +7,5 @@ metadata: {{- include "livebox-exporter.labels" . | nindent 4 }} type: Opaque data: - password: {{ .Values.adminPassword.value | b64enc }} + password: {{ .Values.livebox.adminPassword.value | b64enc }} {{- end }} \ No newline at end of file diff --git a/charts/livebox-exporter/values.yaml b/charts/livebox-exporter/values.yaml index 98120a7..e2a2cee 100644 --- a/charts/livebox-exporter/values.yaml +++ b/charts/livebox-exporter/values.yaml @@ -1,7 +1,3 @@ -# Default values for livebox-exporter. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - replicaCount: 1 image: @@ -10,12 +6,19 @@ image: # Overrides the image tag whose default is the chart appVersion. tag: "" -adminPassword: - secretKeyRef: {} - # name: "" - # key: "" +livebox: + adminPassword: + secretKeyRef: {} + # name: "" + # key: "" - value: "changeme" + value: "changeme" + + # Address of the Livebox. If empty the exporter will use its own default value. + address: "" + + # CA cert of the Livebox. + caCert: "" imagePullSecrets: [] nameOverride: "" diff --git a/go.mod b/go.mod index f497aca..1929fff 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Tomy2e/livebox-exporter go 1.18 require ( - github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7 + github.com/Tomy2e/livebox-api-client v0.0.0-20230524112450-31caca47cbd8 github.com/prometheus/client_golang v1.14.0 golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index acacb92..ada3d6e 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -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/Tomy2e/livebox-api-client v0.0.0-20230524112450-31caca47cbd8 h1:rVK1emWX3JW3nNhLdl76M4RJgmMj/hcgQVOJmrh2r9Y= +github.com/Tomy2e/livebox-api-client v0.0.0-20230524112450-31caca47cbd8/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= diff --git a/internal/collector/deviceinfo.go b/internal/collector/deviceinfo.go index e317cfc..37827d8 100644 --- a/internal/collector/deviceinfo.go +++ b/internal/collector/deviceinfo.go @@ -14,7 +14,7 @@ var _ prometheus.Collector = &DeviceInfo{} // DeviceInfo implements a prometheus Collector that returns Livebox specific metrics. type DeviceInfo struct { - client livebox.Client + client *livebox.Client numberOfRebootsMetric *prometheus.Desc uptimeMetric *prometheus.Desc memoryTotalMetric *prometheus.Desc @@ -22,7 +22,7 @@ type DeviceInfo struct { } // NewDeviceInfo returns a new DeviceInfo collector using the specified client. -func NewDeviceInfo(client livebox.Client) *DeviceInfo { +func NewDeviceInfo(client *livebox.Client) *DeviceInfo { return &DeviceInfo{ client: client, numberOfRebootsMetric: prometheus.NewDesc( diff --git a/internal/poller/devices.go b/internal/poller/devices.go index a244969..5794118 100644 --- a/internal/poller/devices.go +++ b/internal/poller/devices.go @@ -13,12 +13,12 @@ var _ Poller = &DevicesTotal{} // DevicesTotal allows to poll the total number of active devices. type DevicesTotal struct { - client livebox.Client + client *livebox.Client devicesTotal *prometheus.GaugeVec } // NewDevicesTotal returns a new DevicesTotal poller. -func NewDevicesTotal(client livebox.Client) *DevicesTotal { +func NewDevicesTotal(client *livebox.Client) *DevicesTotal { return &DevicesTotal{ client: client, devicesTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{ diff --git a/internal/poller/interface.go b/internal/poller/interface.go index 03c152e..824587a 100644 --- a/internal/poller/interface.go +++ b/internal/poller/interface.go @@ -15,12 +15,12 @@ var _ Poller = &InterfaceMbits{} // InterfaceMbits allows to poll the current bandwidth usage on the Livebox // interfaces. type InterfaceMbits struct { - client livebox.Client + client *livebox.Client txMbits, rxMbits *prometheus.GaugeVec } // NewInterfaceMbits returns a new InterfaceMbits poller. -func NewInterfaceMbits(client livebox.Client) *InterfaceMbits { +func NewInterfaceMbits(client *livebox.Client) *InterfaceMbits { return &InterfaceMbits{ client: client, txMbits: prometheus.NewGaugeVec(prometheus.GaugeOpts{ diff --git a/internal/poller/interface_homelan.go b/internal/poller/interface_homelan.go index 02e882b..368efd9 100644 --- a/internal/poller/interface_homelan.go +++ b/internal/poller/interface_homelan.go @@ -22,14 +22,14 @@ var _ Poller = &InterfaceHomeLanMbits{} // InterfaceHomeLanMbits is an experimental poller to get the current bandwidth // usage on the Livebox interfaces. type InterfaceHomeLanMbits struct { - client livebox.Client + client *livebox.Client interfaces []*exporterLivebox.Interface bitrate *bitrate.Bitrate txMbits, rxMbits *prometheus.GaugeVec } // NewInterfaceHomeLanMbits returns a new InterfaceMbits poller. -func NewInterfaceHomeLanMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceHomeLanMbits { +func NewInterfaceHomeLanMbits(client *livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceHomeLanMbits { return &InterfaceHomeLanMbits{ client: client, interfaces: interfaces, diff --git a/internal/poller/interface_netdev.go b/internal/poller/interface_netdev.go index 306e91f..94966e3 100644 --- a/internal/poller/interface_netdev.go +++ b/internal/poller/interface_netdev.go @@ -16,14 +16,14 @@ var _ Poller = &InterfaceNetDevMbits{} // InterfaceNetDevMbits is an experimental poller to get the current bandwidth // usage on the Livebox interfaces. type InterfaceNetDevMbits struct { - client livebox.Client + client *livebox.Client interfaces []*exporterLivebox.Interface bitrate *bitrate.Bitrate txMbits, rxMbits *prometheus.GaugeVec } // NewInterfaceNetDevMbits returns a new InterfaceNetDevMbits poller. -func NewInterfaceNetDevMbits(client livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceNetDevMbits { +func NewInterfaceNetDevMbits(client *livebox.Client, interfaces []*exporterLivebox.Interface) *InterfaceNetDevMbits { return &InterfaceNetDevMbits{ client: client, interfaces: interfaces, diff --git a/internal/poller/wan.go b/internal/poller/wan.go index 90089f0..058fdc6 100644 --- a/internal/poller/wan.go +++ b/internal/poller/wan.go @@ -14,13 +14,13 @@ var _ Poller = &WANMbits{} // WANMbits is an experimental poller to get the current bandwidth usage on the // WAN interface of the Livebox. type WANMbits struct { - client livebox.Client + client *livebox.Client bitrate *bitrate.Bitrate txMbits, rxMbits prometheus.Gauge } // NewWANMbits returns a new WANMbits poller. -func NewWANMbits(client livebox.Client) *WANMbits { +func NewWANMbits(client *livebox.Client) *WANMbits { return &WANMbits{ client: client, bitrate: bitrate.New(InterfaceHomeLanMbitsMinDelay), diff --git a/main.go b/main.go index fd4b671..98b7aca 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,12 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "errors" "flag" "fmt" + "io/ioutil" "log" "net/http" "os" @@ -37,10 +40,14 @@ var experimentalMetrics = []string{ func parseExperimentalFlag( ctx context.Context, - client livebox.Client, + client *livebox.Client, experimental string, pollingFrequency *uint, ) (pollers []poller.Poller) { + if experimental == "" { + return nil + } + var ( interfaces []*exporterLivebox.Interface err error @@ -96,6 +103,50 @@ func parseExperimentalFlag( return } +func getHTTPClient() (*http.Client, error) { + liveboxCACertPath := os.Getenv("LIVEBOX_CACERT") + + if liveboxCACertPath == "" { + return http.DefaultClient, nil + } + + // Get the SystemCertPool, continue with an empty pool on error. + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + certs, err := ioutil.ReadFile(liveboxCACertPath) + if err != nil { + return nil, fmt.Errorf("failed to read livebox CA cert: %w", err) + } + + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + return nil, errors.New("no livebox CA cert was successfully added") + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + }, + }, nil +} + +func isFatalError(err error) bool { + if errors.Is(err, livebox.ErrInvalidPassword) { + return true + } + + var certError *tls.CertificateVerificationError + if errors.As(err, &certError) { + return true + } + + return false +} + func main() { pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency") listen := flag.String("listen", ":8080", "Listening address") @@ -110,10 +161,28 @@ func main() { log.Fatal("ADMIN_PASSWORD environment variable must be set") } + liveboxAddress := os.Getenv("LIVEBOX_ADDRESS") + if liveboxAddress == "" { + liveboxAddress = livebox.DefaultAddress + } + + httpClient, err := getHTTPClient() + if err != nil { + log.Fatal(err) + } + + client, err := livebox.NewClient( + adminPassword, + livebox.WithAddress(liveboxAddress), + livebox.WithHTTPClient(httpClient), + ) + if err != nil { + log.Fatalf("Failed to create Livebox client: %v", err) + } + var ( ctx = context.Background() registry = prometheus.NewRegistry() - client = livebox.NewClient(adminPassword) pollers = poller.Pollers{ poller.NewDevicesTotal(client), poller.NewInterfaceMbits(client), @@ -136,7 +205,7 @@ func main() { go func() { for { if err := pollers.Poll(ctx); err != nil { - if errors.Is(err, livebox.ErrInvalidPassword) { + if isFatalError(err) { log.Fatal(err) } diff --git a/pkg/livebox/discovery.go b/pkg/livebox/discovery.go index 6e496f7..0433a92 100644 --- a/pkg/livebox/discovery.go +++ b/pkg/livebox/discovery.go @@ -27,7 +27,7 @@ func (i *Interface) IsWLAN() bool { } // DiscoverInterfaces discovers network interfaces on the Livebox. -func DiscoverInterfaces(ctx context.Context, client livebox.Client) ([]*Interface, error) { +func DiscoverInterfaces(ctx context.Context, client *livebox.Client) ([]*Interface, error) { var mibs struct { Status struct { Base map[string]struct {