feat: Add WAN metrics export to Prometheus

Add comprehensive WAN metrics support to unpoller:

WAN Configuration Metrics:
- wan_failover_priority: WAN failover priority
- wan_load_balance_weight: Load balancing weight
- wan_provider_download_kbps: Configured ISP download speed
- wan_provider_upload_kbps: Configured ISP upload speed
- wan_smartq_enabled: SmartQueue QoS status
- wan_magic_enabled: Magic WAN status
- wan_vlan_enabled: VLAN configuration status

WAN Statistics Metrics:
- wan_uptime_percentage: WAN uptime percentage
- wan_peak_download_percent: Peak download utilization
- wan_peak_upload_percent: Peak upload utilization
- wan_max_rx_bytes_rate: Maximum receive rate
- wan_max_tx_bytes_rate: Maximum transmit rate

WAN Service Provider Metrics:
- wan_service_provider_asn: ISP autonomous system number

Labels include:
- wan_id, wan_name, wan_networkgroup
- wan_type (dhcp, static, pppoe)
- wan_load_balance_type (weighted, failover-only)
- isp_name, isp_city (service provider metrics)
- site_name, source

Changes:
- pkg/poller/config.go: Add WANConfigs field to Metrics struct
- pkg/poller/inputs.go: Append WAN configs in metric aggregation
- pkg/inputunifi/input.go: Add WANConfigs field to Metrics struct
- pkg/inputunifi/collector.go: Fetch WAN enriched configuration
- pkg/promunifi/wan.go: New WAN metrics exporter
- pkg/promunifi/collector.go: Initialize and export WAN metrics

Depends on: unpoller/unifi PR (WAN API support)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
brngates98
2026-01-29 17:24:12 -05:00
parent d178da7c75
commit aac4917da7
7 changed files with 176 additions and 2 deletions

2
go.mod
View File

@@ -47,4 +47,4 @@ require (
google.golang.org/protobuf v1.36.11 // indirect
)
// replace github.com/unpoller/unifi/v5 => ../unifi
replace github.com/unpoller/unifi/v5 => ../unifi

View File

@@ -190,6 +190,14 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
u.LogDebugf("Found %d DHCPLeases entries", len(m.DHCPLeases))
}
// Get WAN enriched configuration
if m.WANConfigs, err = c.Unifi.GetWANEnrichedConfiguration(sites); err != nil {
// Don't fail collection if WAN config fails - older controllers may not have this endpoint
u.LogDebugf("unifi.GetWANEnrichedConfiguration(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d WAN configuration entries", len(m.WANConfigs))
}
return u.augmentMetrics(c, m), nil
}
@@ -383,6 +391,12 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
m.DHCPLeases = append(m.DHCPLeases, lease)
}
for _, wanConfig := range metrics.WANConfigs {
// WANEnrichedConfiguration doesn't have a SiteName field by default
// The site context is preserved via the collector's site list
m.WANConfigs = append(m.WANConfigs, wanConfig)
}
// Apply default_site_name_override to all metrics if configured.
// This must be done AFTER all metrics are added to m, so everything is included.
// This allows us to use the console name for Cloud Gateways while keeping
@@ -487,6 +501,14 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
}
}
}
// Apply to WAN configs
for i := range m.WANConfigs {
if wanConfig, ok := m.WANConfigs[i].(*unifi.WANEnrichedConfiguration); ok {
// WAN configs don't have SiteName field, but we'll add it in the exporter
_ = wanConfig
}
}
}
// this is a helper function for augmentMetrics.

View File

@@ -87,6 +87,7 @@ type Metrics struct {
SpeedTests []*unifi.SpeedTestResult
Devices *unifi.Devices
DHCPLeases []*unifi.DHCPLease
WANConfigs []*unifi.WANEnrichedConfiguration
}
func init() { // nolint: gochecknoinits

View File

@@ -89,6 +89,7 @@ type Metrics struct {
SpeedTests []any
CountryTraffic []any
DHCPLeases []any
WANConfigs []any
}
// Events defines the type for log entries.

View File

@@ -270,6 +270,7 @@ func AppendMetrics(existing *Metrics, m *Metrics) *Metrics {
existing.Devices = append(existing.Devices, m.Devices...)
existing.CountryTraffic = append(existing.CountryTraffic, m.CountryTraffic...)
existing.DHCPLeases = append(existing.DHCPLeases, m.DHCPLeases...)
existing.WANConfigs = append(existing.WANConfigs, m.WANConfigs...)
return existing
}

View File

@@ -48,6 +48,7 @@ type promUnifi struct {
SpeedTest *speedtest
CountryTraffic *ucountrytraffic
DHCPLease *dhcplease
WAN *wan
// 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
@@ -208,6 +209,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
u.SpeedTest = descSpeedTest(u.Namespace + "_speedtest_")
u.CountryTraffic = descCountryTraffic(u.Namespace + "_countrytraffic_")
u.DHCPLease = descDHCPLease(u.Namespace + "_")
u.WAN = descWAN(u.Namespace + "_")
mux := http.NewServeMux()
promver.Version = version.Version
@@ -291,7 +293,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, u.SpeedTest, u.DHCPLease} {
for _, f := range []any{u.Client, u.Device, u.UAP, u.USG, u.USW, u.PDU, u.Site, u.SpeedTest, u.DHCPLease, u.WAN} {
v := reflect.Indirect(reflect.ValueOf(f))
// Loop each struct member and send it to the provided channel.
@@ -432,6 +434,13 @@ func (u *promUnifi) loopExports(r report) {
}
}
// Export WAN metrics
for _, wanConfig := range m.WANConfigs {
if w, ok := wanConfig.(*unifi.WANEnrichedConfiguration); ok {
u.exportWAN(r, w)
}
}
u.exportClientDPItotals(r, appTotal, catTotal)
}

140
pkg/promunifi/wan.go Normal file
View File

@@ -0,0 +1,140 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/unpoller/unifi/v5"
)
type wan struct {
// WAN Configuration metrics
FailoverPriority *prometheus.Desc
LoadBalanceWeight *prometheus.Desc
ProviderDownloadKbps *prometheus.Desc
ProviderUploadKbps *prometheus.Desc
SmartQEnabled *prometheus.Desc
MagicEnabled *prometheus.Desc
VlanEnabled *prometheus.Desc
// WAN Statistics metrics
UptimePercentage *prometheus.Desc
PeakDownloadPercent *prometheus.Desc
PeakUploadPercent *prometheus.Desc
MaxRxBytesR *prometheus.Desc
MaxTxBytesR *prometheus.Desc
// WAN Service Provider metrics
ServiceProviderASN *prometheus.Desc
// WAN Creation timestamp
CreationTimestamp *prometheus.Desc
}
func descWAN(ns string) *wan {
labels := []string{
"wan_id",
"wan_name",
"wan_networkgroup",
"wan_type",
"wan_load_balance_type",
"site_name",
"source",
}
providerLabels := []string{
"wan_id",
"wan_name",
"wan_networkgroup",
"isp_name",
"isp_city",
"site_name",
"source",
}
nd := prometheus.NewDesc
return &wan{
// Configuration
FailoverPriority: nd(ns+"wan_failover_priority", "WAN failover priority (lower is higher priority)", labels, nil),
LoadBalanceWeight: nd(ns+"wan_load_balance_weight", "WAN load balancing weight", labels, nil),
ProviderDownloadKbps: nd(ns+"wan_provider_download_kbps", "Configured ISP download speed in Kbps", labels, nil),
ProviderUploadKbps: nd(ns+"wan_provider_upload_kbps", "Configured ISP upload speed in Kbps", labels, nil),
SmartQEnabled: nd(ns+"wan_smartq_enabled", "SmartQueue QoS enabled (1) or disabled (0)", labels, nil),
MagicEnabled: nd(ns+"wan_magic_enabled", "Magic WAN enabled (1) or disabled (0)", labels, nil),
VlanEnabled: nd(ns+"wan_vlan_enabled", "VLAN enabled for WAN (1) or disabled (0)", labels, nil),
// Statistics
UptimePercentage: nd(ns+"wan_uptime_percentage", "WAN uptime percentage", labels, nil),
PeakDownloadPercent: nd(ns+"wan_peak_download_percent", "Peak download usage as percentage of configured capacity", labels, nil),
PeakUploadPercent: nd(ns+"wan_peak_upload_percent", "Peak upload usage as percentage of configured capacity", labels, nil),
MaxRxBytesR: nd(ns+"wan_max_rx_bytes_rate", "Maximum receive bytes rate", labels, nil),
MaxTxBytesR: nd(ns+"wan_max_tx_bytes_rate", "Maximum transmit bytes rate", labels, nil),
// Service Provider
ServiceProviderASN: nd(ns+"wan_service_provider_asn", "Service provider autonomous system number", providerLabels, nil),
// Creation
CreationTimestamp: nd(ns+"wan_creation_timestamp", "WAN configuration creation timestamp", labels, nil),
}
}
func (u *promUnifi) exportWAN(r report, w *unifi.WANEnrichedConfiguration) {
if w == nil {
return
}
cfg := w.Configuration
stats := w.Statistics
details := w.Details
// Base labels
labels := []string{
cfg.ID,
cfg.Name,
cfg.WANNetworkgroup,
cfg.WANType,
cfg.WANLoadBalanceType,
"", // site_name - will be set by caller if available
"", // source - will be set by caller if available
}
// Convert boolean FlexBool values to float64
smartQEnabled := 0.0
if cfg.WANSmartqEnabled.Val {
smartQEnabled = 1.0
}
magicEnabled := 0.0
if cfg.WANMagicEnabled.Val {
magicEnabled = 1.0
}
vlanEnabled := 0.0
if cfg.WANVlanEnabled.Val {
vlanEnabled = 1.0
}
metrics := []*metric{
{u.WAN.FailoverPriority, gauge, cfg.WANFailoverPriority.Val, labels},
{u.WAN.LoadBalanceWeight, gauge, cfg.WANLoadBalanceWeight.Val, labels},
{u.WAN.ProviderDownloadKbps, gauge, cfg.WANProviderCapabilities.DownloadKbps.Val, labels},
{u.WAN.ProviderUploadKbps, gauge, cfg.WANProviderCapabilities.UploadKbps.Val, labels},
{u.WAN.SmartQEnabled, gauge, smartQEnabled, labels},
{u.WAN.MagicEnabled, gauge, magicEnabled, labels},
{u.WAN.VlanEnabled, gauge, vlanEnabled, labels},
{u.WAN.UptimePercentage, gauge, stats.UptimePercentage, labels},
{u.WAN.PeakDownloadPercent, gauge, stats.PeakUsage.DownloadPercentage, labels},
{u.WAN.PeakUploadPercent, gauge, stats.PeakUsage.UploadPercentage, labels},
{u.WAN.MaxRxBytesR, gauge, stats.PeakUsage.MaxRxBytesR.Val, labels},
{u.WAN.MaxTxBytesR, gauge, stats.PeakUsage.MaxTxBytesR.Val, labels},
{u.WAN.CreationTimestamp, gauge, details.CreationTimestamp.Val, labels},
}
// Service provider info (uses different labels)
providerLabels := []string{
cfg.ID,
cfg.Name,
cfg.WANNetworkgroup,
details.ServiceProvider.Name,
details.ServiceProvider.City,
"", // site_name
"", // source
}
metrics = append(metrics, &metric{u.WAN.ServiceProviderASN, gauge, details.ServiceProvider.ASN.Val, providerLabels})
r.send(metrics)
}