diff --git a/go.mod b/go.mod index 6932c018..1a1fb466 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/inputunifi/collector.go b/pkg/inputunifi/collector.go index a86e827d..50bfde35 100644 --- a/pkg/inputunifi/collector.go +++ b/pkg/inputunifi/collector.go @@ -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. diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go index 9fe762c5..18db4a63 100644 --- a/pkg/inputunifi/input.go +++ b/pkg/inputunifi/input.go @@ -87,6 +87,7 @@ type Metrics struct { SpeedTests []*unifi.SpeedTestResult Devices *unifi.Devices DHCPLeases []*unifi.DHCPLease + WANConfigs []*unifi.WANEnrichedConfiguration } func init() { // nolint: gochecknoinits diff --git a/pkg/poller/config.go b/pkg/poller/config.go index 2e622329..cb09f31c 100644 --- a/pkg/poller/config.go +++ b/pkg/poller/config.go @@ -89,6 +89,7 @@ type Metrics struct { SpeedTests []any CountryTraffic []any DHCPLeases []any + WANConfigs []any } // Events defines the type for log entries. diff --git a/pkg/poller/inputs.go b/pkg/poller/inputs.go index 9822ff69..a5e848f2 100644 --- a/pkg/poller/inputs.go +++ b/pkg/poller/inputs.go @@ -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 } diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index 05e799fe..07e41e1d 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -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) } diff --git a/pkg/promunifi/wan.go b/pkg/promunifi/wan.go new file mode 100644 index 00000000..917aa441 --- /dev/null +++ b/pkg/promunifi/wan.go @@ -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) +}