mirror of
https://github.com/unpoller/unpoller.git
synced 2026-04-05 08:54:00 -04:00
Fix multi-WAN speed test reporting (issue #841)
Speed tests were not being reported correctly for multi-WAN setups
because the device-level speedtest-status field was returning zeros.
The data has moved to a new aggregated dashboard API endpoint.
Changes:
- Add GetSpeedTests() and GetSiteSpeedTests() methods to fetch from
/v2/api/site/{site}/aggregated-dashboard endpoint
- Create SpeedTestResult data structures to capture per-WAN metrics
- Update Prometheus exporter with new speedtest_* metrics per interface
- Update InfluxDB exporter to write speedtest measurements per WAN
- Update Datadog exporter with unifi.speedtest.* metrics per WAN
- Update metrics collection to include speed test data for all sites
Metrics now include labels/tags for:
- wan_interface: Physical interface (eth8, eth9, etc.)
- wan_group: Logical WAN name (WAN, WAN2, etc.)
- site_name: Site identifier
- source: Controller URL
Gracefully handles older controllers without the new API endpoint.
Fixes #841
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
39
pkg/datadogunifi/speedtest.go
Normal file
39
pkg/datadogunifi/speedtest.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
35
pkg/influxunifi/speedtest.go
Normal file
35
pkg/influxunifi/speedtest.go
Normal 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})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ type Metrics struct {
|
||||
SitesDPI []*unifi.DPITable
|
||||
ClientsDPI []*unifi.DPITable
|
||||
RogueAPs []*unifi.RogueAP
|
||||
SpeedTests []*unifi.SpeedTestResult
|
||||
Devices *unifi.Devices
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ type Metrics struct {
|
||||
ClientsDPI []any
|
||||
Devices []any
|
||||
RogueAPs []any
|
||||
SpeedTests []any
|
||||
}
|
||||
|
||||
// Events defines the type for log entries.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
39
pkg/promunifi/speedtest.go
Normal file
39
pkg/promunifi/speedtest.go
Normal 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},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user