Set new metrics as experimental

This commit is contained in:
Tomy Guichard 2023-03-18 18:56:02 +01:00
parent 6cf0fa27ec
commit b008540c3f
12 changed files with 629 additions and 253 deletions

View file

@ -0,0 +1,9 @@
package poller
import "math"
const maxMbits = 2150
func sanitizeMbits(mbits float64) float64 {
return math.Min(mbits, maxMbits)
}

View file

@ -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

View 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
}

View 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
View 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
}