Set new metrics as experimental
This commit is contained in:
parent
6cf0fa27ec
commit
b008540c3f
12 changed files with 629 additions and 253 deletions
28
README.md
28
README.md
|
@ -7,11 +7,20 @@ FTTH subscription.
|
||||||
|
|
||||||
This exporter currently exposes the following metrics:
|
This exporter currently exposes the following metrics:
|
||||||
|
|
||||||
| Name | Type | Description | Labels |
|
| Name | Type | Description | Labels | Experimental |
|
||||||
| -------------------------- | ----- | ---------------------------------- | --------- |
|
| ---------------------------------- | ----- | ------------------------------------------------- | --------- | ------------ |
|
||||||
| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface |
|
| livebox_interface_rx_mbits | gauge | Received Mbits per second | interface | No |
|
||||||
| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface |
|
| livebox_interface_tx_mbits | gauge | Transmitted Mbits per second | interface | No |
|
||||||
| livebox_devices_total | gauge | The total number of active devices | type |
|
| 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
|
## Usage
|
||||||
|
|
||||||
|
@ -19,10 +28,11 @@ This exporter currently exposes the following metrics:
|
||||||
|
|
||||||
The exporter accepts the following command-line options:
|
The exporter accepts the following command-line options:
|
||||||
|
|
||||||
| Name | Description | Default value |
|
| Name | Description | Default value |
|
||||||
| ------------------- | ----------------- | ------------- |
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
|
||||||
| --polling-frequency | Polling frequency | 30 |
|
| -polling-frequency | Polling frequency | 30 |
|
||||||
| --listen | Listening address | :8080 |
|
| -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:
|
The exporter reads the following environment variables:
|
||||||
|
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -5,6 +5,7 @@ go 1.18
|
||||||
require (
|
require (
|
||||||
github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7
|
github.com/Tomy2e/livebox-api-client v0.0.0-20230304114924-a629a6a185e7
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
|
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +17,6 @@ require (
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.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
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
6
go.sum
6
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-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-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-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-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/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=
|
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-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-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-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|
9
internal/poller/common.go
Normal file
9
internal/poller/common.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package poller
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const maxMbits = 2150
|
||||||
|
|
||||||
|
func sanitizeMbits(mbits float64) float64 {
|
||||||
|
return math.Min(mbits, maxMbits)
|
||||||
|
}
|
|
@ -2,15 +2,12 @@ package poller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Tomy2e/livebox-api-client"
|
"github.com/Tomy2e/livebox-api-client"
|
||||||
"github.com/Tomy2e/livebox-api-client/api/request"
|
"github.com/Tomy2e/livebox-api-client/api/request"
|
||||||
|
"github.com/Tomy2e/livebox-exporter/pkg/bitrate"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Poller = &InterfaceMbits{}
|
var _ Poller = &InterfaceMbits{}
|
||||||
|
@ -18,26 +15,8 @@ var _ Poller = &InterfaceMbits{}
|
||||||
// InterfaceMbits allows to poll the current bandwidth usage on the Livebox
|
// InterfaceMbits allows to poll the current bandwidth usage on the Livebox
|
||||||
// interfaces.
|
// interfaces.
|
||||||
type InterfaceMbits struct {
|
type InterfaceMbits struct {
|
||||||
client livebox.Client
|
client livebox.Client
|
||||||
txMbits, rxMbits *prometheus.GaugeVec
|
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.
|
// NewInterfaceMbits returns a new InterfaceMbits poller.
|
||||||
|
@ -58,237 +37,57 @@ func NewInterfaceMbits(client livebox.Client) *InterfaceMbits {
|
||||||
// Name of the interface.
|
// Name of the interface.
|
||||||
"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.
|
// Collectors returns all metrics.
|
||||||
func (im *InterfaceMbits) Collectors() []prometheus.Collector {
|
func (im *InterfaceMbits) Collectors() []prometheus.Collector {
|
||||||
return []prometheus.Collector{
|
return []prometheus.Collector{im.txMbits, im.rxMbits}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll polls the current bandwidth usage.
|
// Poll polls the current bandwidth usage.
|
||||||
func (im *InterfaceMbits) Poll(ctx context.Context) error {
|
func (im *InterfaceMbits) Poll(ctx context.Context) error {
|
||||||
if len(im.interfaces) == 0 {
|
var counters struct {
|
||||||
if err := im.discoverInterfaces(ctx); err != nil {
|
Status map[string]struct {
|
||||||
return err
|
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 {
|
for iface, traffic := range counters.Status {
|
||||||
return im.pollInterfaces(ctx)
|
rxCounter := 0
|
||||||
})
|
txCounter := 0
|
||||||
|
|
||||||
eg.Go(func() error {
|
if len(traffic.Traffic) > 0 {
|
||||||
return im.pollInterfacesNetDev(ctx)
|
rxCounter = traffic.Traffic[0].RxCounter
|
||||||
})
|
txCounter = traffic.Traffic[0].TxCounter
|
||||||
|
|
||||||
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 {
|
im.rxMbits.
|
||||||
Status struct {
|
With(prometheus.Labels{"interface": iface}).
|
||||||
BytesReceived int64 `json:"BytesReceived"`
|
Set(bitrate.BitsPer30SecsToMbits(rxCounter))
|
||||||
BytesSent int64 `json:"BytesSent"`
|
im.txMbits.
|
||||||
} `json:"status"`
|
With(prometheus.Labels{"interface": iface}).
|
||||||
}
|
Set(bitrate.BitsPer30SecsToMbits(txCounter))
|
||||||
|
|
||||||
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
|
return nil
|
||||||
|
|
110
internal/poller/interface_homelan.go
Normal file
110
internal/poller/interface_homelan.go
Normal file
|
@ -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
|
||||||
|
}
|
99
internal/poller/interface_netdev.go
Normal file
99
internal/poller/interface_netdev.go
Normal file
|
@ -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
|
||||||
|
}
|
81
internal/poller/wan.go
Normal file
81
internal/poller/wan.go
Normal file
|
@ -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
|
||||||
|
}
|
86
main.go
86
main.go
|
@ -4,23 +4,104 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Tomy2e/livebox-api-client"
|
"github.com/Tomy2e/livebox-api-client"
|
||||||
"github.com/Tomy2e/livebox-exporter/internal/poller"
|
"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"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"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() {
|
func main() {
|
||||||
pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency")
|
pollingFrequency := flag.Uint("polling-frequency", defaultPollingFrequency, "Polling frequency")
|
||||||
listen := flag.String("listen", ":8080", "Listening address")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
adminPassword := os.Getenv("ADMIN_PASSWORD")
|
adminPassword := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
@ -38,6 +119,9 @@ func main() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add experimental pollers.
|
||||||
|
pollers = append(pollers, parseExperimentalFlag(ctx, client, *experimental, pollingFrequency)...)
|
||||||
|
|
||||||
registry.MustRegister(
|
registry.MustRegister(
|
||||||
append(
|
append(
|
||||||
pollers.Collectors(),
|
pollers.Collectors(),
|
||||||
|
|
106
pkg/bitrate/bitrate.go
Normal file
106
pkg/bitrate/bitrate.go
Normal file
|
@ -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
|
||||||
|
}
|
11
pkg/bitrate/conversion.go
Normal file
11
pkg/bitrate/conversion.go
Normal file
|
@ -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
|
||||||
|
}
|
64
pkg/livebox/discovery.go
Normal file
64
pkg/livebox/discovery.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue