Add new experimental metrics (#4)

This commit is contained in:
Tomy Guichard 2023-04-21 18:58:34 +02:00 committed by GitHub
parent 4bac289600
commit 136f95a9b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 592 additions and 18 deletions

View file

@ -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 | | 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 ## 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
View file

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

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

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

@ -6,6 +6,7 @@ import (
"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"
) )
@ -44,10 +45,6 @@ func (im *InterfaceMbits) Collectors() []prometheus.Collector {
return []prometheus.Collector{im.txMbits, im.rxMbits} return []prometheus.Collector{im.txMbits, im.rxMbits}
} }
func bitsPer30SecsToMbitsPerSec(v int) float64 {
return float64(v) / 30000000
}
// 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 {
var counters struct { var counters struct {
@ -87,10 +84,10 @@ func (im *InterfaceMbits) Poll(ctx context.Context) error {
im.rxMbits. im.rxMbits.
With(prometheus.Labels{"interface": iface}). With(prometheus.Labels{"interface": iface}).
Set(bitsPer30SecsToMbitsPerSec(rxCounter)) Set(bitrate.BitsPer30SecsToMbits(rxCounter))
im.txMbits. im.txMbits.
With(prometheus.Labels{"interface": iface}). With(prometheus.Labels{"interface": iface}).
Set(bitsPer30SecsToMbitsPerSec(txCounter)) Set(bitrate.BitsPer30SecsToMbits(txCounter))
} }
return nil 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
}

84
main.go
View file

@ -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 = 30 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
View 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
View 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
View 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
}