Merge pull request #896 from unpoller/issue-841-fix-multi-wan-speedtests

Fix multi-WAN speed test reporting (issue #841)
This commit is contained in:
Cody Lee
2025-12-09 16:53:56 -06:00
committed by GitHub
11 changed files with 157 additions and 12 deletions

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/prometheus/common v0.67.4
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/unpoller/unifi/v5 v5.2.1
github.com/unpoller/unifi/v5 v5.3.0
golang.org/x/crypto v0.46.0
golang.org/x/term v0.38.0
golift.io/cnfg v0.2.3

4
go.sum
View File

@@ -75,8 +75,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/unpoller/unifi/v5 v5.2.1 h1:clcF0/UKYQm4ycWlM0Pe6f+NbmGGpky3KkfuGBBmsR0=
github.com/unpoller/unifi/v5 v5.2.1/go.mod h1:a9Hl1hBnDuaJDIvHswpW8/QUQgk3gQ5U9c5EnpZXMUg=
github.com/unpoller/unifi/v5 v5.3.0 h1:6ykCP4wL5nk/icMu8Qc24ApWD0A5JdCSYxQJIg2FQyg=
github.com/unpoller/unifi/v5 v5.3.0/go.mod h1:pa6zv4Oyb1nFEm4qu/8CUv8Q25hQof04Wh2D0RXcTYc=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=

View File

@@ -302,6 +302,10 @@ func (u *DatadogUnifi) loopPoints(r report) {
u.switchExport(r, s)
}
for _, st := range m.SpeedTests {
u.switchExport(r, st)
}
for _, s := range r.events().Logs {
u.switchExport(r, s)
}
@@ -348,6 +352,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
u.batchAlarms(r, v)
case *unifi.Anomaly:
u.batchAnomaly(r, v)
case *unifi.SpeedTestResult:
u.batchSpeedTest(r, v)
default:
u.LogErrorf("invalid export, type=%+v", reflect.TypeOf(v))
}

View File

@@ -0,0 +1,39 @@
package datadogunifi
import (
"github.com/unpoller/unifi/v5"
)
// batchSpeedTest generates Unifi Speed Test datapoints for Datadog.
// These points can be passed directly to Datadog.
func (u *DatadogUnifi) batchSpeedTest(r report, st *unifi.SpeedTestResult) {
if st == nil {
return
}
metricName := metricNamespace("speedtest")
tags := []string{
tag("site_name", st.SiteName),
tag("source", st.SourceName),
tag("wan_interface", st.InterfaceName),
tag("wan_group", st.WANNetworkGroup),
tag("network_conf_id", st.NetworkConfID),
}
data := map[string]float64{
"download_mbps": st.DownloadMbps.Val,
"upload_mbps": st.UploadMbps.Val,
"latency_ms": st.LatencyMs.Val,
"timestamp": st.Time.Val,
}
if st.WANProviderCapabilities != nil {
data["provider_download_kbps"] = st.WANProviderCapabilities.DownloadKbps.Val
data["provider_upload_kbps"] = st.WANProviderCapabilities.UploadKbps.Val
}
for name, value := range data {
_ = r.reportGauge(metricName(name), value, tags)
}
}

View File

@@ -414,6 +414,10 @@ func (u *InfluxUnifi) loopPoints(r report) {
u.switchExport(r, s)
}
for _, st := range m.SpeedTests {
u.switchExport(r, st)
}
for _, s := range r.events().Logs {
u.switchExport(r, s)
}
@@ -460,6 +464,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
u.batchAlarms(r, v)
case *unifi.Anomaly:
u.batchAnomaly(r, v)
case *unifi.SpeedTestResult:
u.batchSpeedTest(r, v)
default:
u.LogErrorf("invalid export type: %T", v)
}

View File

@@ -0,0 +1,35 @@
package influxunifi
import (
"github.com/unpoller/unifi/v5"
)
// batchSpeedTest generates Unifi Speed Test datapoints for InfluxDB.
// These points can be passed directly to influx.
func (u *InfluxUnifi) batchSpeedTest(r report, st *unifi.SpeedTestResult) {
if st == nil {
return
}
tags := map[string]string{
"site_name": st.SiteName,
"source": st.SourceName,
"wan_interface": st.InterfaceName,
"wan_group": st.WANNetworkGroup,
"network_conf_id": st.NetworkConfID,
}
fields := map[string]any{
"download_mbps": st.DownloadMbps.Val,
"upload_mbps": st.UploadMbps.Val,
"latency_ms": st.LatencyMs.Val,
"timestamp": st.Time.Val,
}
if st.WANProviderCapabilities != nil {
fields["provider_download_kbps"] = st.WANProviderCapabilities.DownloadKbps.Val
fields["provider_upload_kbps"] = st.WANProviderCapabilities.UploadKbps.Val
}
r.send(&metric{Table: "speedtest", Tags: tags, Fields: fields})
}

View File

@@ -124,6 +124,12 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
return nil, fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err)
}
// Get speed test results for all WANs
if m.SpeedTests, err = c.Unifi.GetSpeedTests(sites, 86400); err != nil {
// Don't fail collection if speed tests fail - older controllers may not have this endpoint
u.LogDebugf("unifi.GetSpeedTests(%s): %v (continuing)", c.URL, err)
}
return u.augmentMetrics(c, m), nil
}
@@ -181,6 +187,10 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
}
}
for _, speedTest := range metrics.SpeedTests {
m.SpeedTests = append(m.SpeedTests, speedTest)
}
return m
}

View File

@@ -72,6 +72,7 @@ type Metrics struct {
SitesDPI []*unifi.DPITable
ClientsDPI []*unifi.DPITable
RogueAPs []*unifi.RogueAP
SpeedTests []*unifi.SpeedTestResult
Devices *unifi.Devices
}

View File

@@ -86,6 +86,7 @@ type Metrics struct {
ClientsDPI []any
Devices []any
RogueAPs []any
SpeedTests []any
}
// Events defines the type for log entries.

View File

@@ -36,14 +36,15 @@ var ErrMetricFetchFailed = fmt.Errorf("metric fetch failed")
type promUnifi struct {
*Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"`
Client *uclient
Device *unifiDevice
UAP *uap
USG *usg
USW *usw
PDU *pdu
Site *site
RogueAP *rogueap
Client *uclient
Device *unifiDevice
UAP *uap
USG *usg
USW *usw
PDU *pdu
Site *site
RogueAP *rogueap
SpeedTest *speedtest
// This interface is passed to the Collect() method. The Collect method uses
// this interface to retrieve the latest UniFi measurements and export them.
Collector poller.Collect
@@ -200,6 +201,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
u.PDU = descPDU(u.Namespace + "_device_")
u.Site = descSite(u.Namespace + "_site_")
u.RogueAP = descRogueAP(u.Namespace + "_rogueap_")
u.SpeedTest = descSpeedTest(u.Namespace + "_speedtest_")
mux := http.NewServeMux()
promver.Version = version.Version
@@ -283,7 +285,7 @@ func (t *target) Describe(ch chan<- *prometheus.Desc) {
// Describe satisfies the prometheus Collector. This returns all of the
// metric descriptions that this packages produces.
func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) {
for _, f := range []any{u.Client, u.Device, u.UAP, u.USG, u.USW, u.PDU, u.Site} {
for _, f := range []any{u.Client, u.Device, u.UAP, u.USG, u.USW, u.PDU, u.Site, u.SpeedTest} {
v := reflect.Indirect(reflect.ValueOf(f))
// Loop each struct member and send it to the provided channel.
@@ -391,6 +393,10 @@ func (u *promUnifi) loopExports(r report) {
u.switchExport(r, d)
}
for _, st := range m.SpeedTests {
u.switchExport(r, st)
}
appTotal := make(totalsDPImap)
catTotal := make(totalsDPImap)
@@ -434,6 +440,8 @@ func (u *promUnifi) switchExport(r report, v any) {
u.exportSite(r, v)
case *unifi.Client:
u.exportClient(r, v)
case *unifi.SpeedTestResult:
u.exportSpeedTest(r, v)
default:
u.LogErrorf("invalid type: %T", v)
}

View File

@@ -0,0 +1,39 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/unpoller/unifi/v5"
)
type speedtest struct {
DownloadMbps *prometheus.Desc
UploadMbps *prometheus.Desc
LatencyMs *prometheus.Desc
Timestamp *prometheus.Desc
}
func descSpeedTest(ns string) *speedtest {
labels := []string{"wan_interface", "wan_group", "site_name", "source"}
return &speedtest{
DownloadMbps: prometheus.NewDesc(ns+"download_mbps", "Speed Test Download in Mbps", labels, nil),
UploadMbps: prometheus.NewDesc(ns+"upload_mbps", "Speed Test Upload in Mbps", labels, nil),
LatencyMs: prometheus.NewDesc(ns+"latency_ms", "Speed Test Latency in milliseconds", labels, nil),
Timestamp: prometheus.NewDesc(ns+"timestamp_seconds", "Speed Test Timestamp (Unix epoch)", labels, nil),
}
}
func (u *promUnifi) exportSpeedTest(r report, st *unifi.SpeedTestResult) {
if st == nil {
return
}
labels := []string{st.InterfaceName, st.WANNetworkGroup, st.SiteName, st.SourceName}
r.send([]*metric{
{u.SpeedTest.DownloadMbps, gauge, st.DownloadMbps, labels},
{u.SpeedTest.UploadMbps, gauge, st.UploadMbps, labels},
{u.SpeedTest.LatencyMs, gauge, st.LatencyMs, labels},
{u.SpeedTest.Timestamp, gauge, st.Time, labels},
})
}