diff --git a/go.mod b/go.mod index 95c8edc2..66359e26 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus/common v0.67.5 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - github.com/unpoller/unifi/v5 v5.14.0 + github.com/unpoller/unifi/v5 v5.15.0 golang.org/x/crypto v0.47.0 golang.org/x/term v0.39.0 golift.io/cnfg v0.2.3 diff --git a/go.sum b/go.sum index f6193f20..95754130 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,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.14.0 h1:mKz1GJhkYStJLgP6CpHAvMl8YTqBFJDCM8TNvC62Ois= -github.com/unpoller/unifi/v5 v5.14.0/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo= +github.com/unpoller/unifi/v5 v5.15.0 h1:9xYBmboWBcY4Cv8ARbWMjBlAUNVlG7TIuX+aRf6mcUE= +github.com/unpoller/unifi/v5 v5.15.0/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo= 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= diff --git a/pkg/inputunifi/collector.go b/pkg/inputunifi/collector.go index 50bfde35..68e48489 100644 --- a/pkg/inputunifi/collector.go +++ b/pkg/inputunifi/collector.go @@ -198,6 +198,14 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { u.LogDebugf("Found %d WAN configuration entries", len(m.WANConfigs)) } + // Get controller system info (UniFi OS only) + if m.Sysinfos, err = c.Unifi.GetSysinfo(sites); err != nil { + // Don't fail collection if sysinfo fails - older controllers may not have this endpoint + u.LogDebugf("unifi.GetSysinfo(%s): %v (continuing)", c.URL, err) + } else { + u.LogDebugf("Found %d Sysinfo entries", len(m.Sysinfos)) + } + return u.augmentMetrics(c, m), nil } @@ -397,6 +405,10 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met m.WANConfigs = append(m.WANConfigs, wanConfig) } + for _, sysinfo := range metrics.Sysinfos { + m.Sysinfos = append(m.Sysinfos, sysinfo) + } + // 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 @@ -502,6 +514,15 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) { } } + // Apply to sysinfo (controller metrics) + for i := range m.Sysinfos { + if s, ok := m.Sysinfos[i].(*unifi.Sysinfo); ok { + if isDefaultSiteName(s.SiteName) { + s.SiteName = overrideName + } + } + } + // Apply to WAN configs for i := range m.WANConfigs { if wanConfig, ok := m.WANConfigs[i].(*unifi.WANEnrichedConfiguration); ok { diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go index 18db4a63..cd04c126 100644 --- a/pkg/inputunifi/input.go +++ b/pkg/inputunifi/input.go @@ -88,6 +88,7 @@ type Metrics struct { Devices *unifi.Devices DHCPLeases []*unifi.DHCPLease WANConfigs []*unifi.WANEnrichedConfiguration + Sysinfos []*unifi.Sysinfo } func init() { // nolint: gochecknoinits diff --git a/pkg/poller/config.go b/pkg/poller/config.go index 033abc99..d53fd464 100644 --- a/pkg/poller/config.go +++ b/pkg/poller/config.go @@ -92,6 +92,7 @@ type Metrics struct { CountryTraffic []any DHCPLeases []any WANConfigs []any + Sysinfos []any } // Events defines the type for log entries. diff --git a/pkg/poller/inputs.go b/pkg/poller/inputs.go index f61deb1d..c15ad043 100644 --- a/pkg/poller/inputs.go +++ b/pkg/poller/inputs.go @@ -24,8 +24,7 @@ type Input interface { DebugInput() (bool, error) } -// Discoverer is an optional interface for inputs that can discover API endpoints -// on a controller and write a shareable report (e.g. for support/debugging). +// Discoverer is an optional interface for inputs that can discover API endpoints. type Discoverer interface { Discover(outputPath string) error } @@ -277,6 +276,7 @@ func AppendMetrics(existing *Metrics, m *Metrics) *Metrics { existing.CountryTraffic = append(existing.CountryTraffic, m.CountryTraffic...) existing.DHCPLeases = append(existing.DHCPLeases, m.DHCPLeases...) existing.WANConfigs = append(existing.WANConfigs, m.WANConfigs...) + existing.Sysinfos = append(existing.Sysinfos, m.Sysinfos...) return existing } diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index 07e41e1d..efe06a16 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -49,6 +49,7 @@ type promUnifi struct { CountryTraffic *ucountrytraffic DHCPLease *dhcplease WAN *wan + Controller *controller // 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 @@ -210,6 +211,7 @@ func (u *promUnifi) Run(c poller.Collect) error { u.CountryTraffic = descCountryTraffic(u.Namespace + "_countrytraffic_") u.DHCPLease = descDHCPLease(u.Namespace + "_") u.WAN = descWAN(u.Namespace + "_") + u.Controller = descController(u.Namespace + "_") mux := http.NewServeMux() promver.Version = version.Version @@ -441,6 +443,13 @@ func (u *promUnifi) loopExports(r report) { } } + // Export controller sysinfo metrics + for _, s := range m.Sysinfos { + if sysinfo, ok := s.(*unifi.Sysinfo); ok { + u.exportSysinfo(r, sysinfo) + } + } + u.exportClientDPItotals(r, appTotal, catTotal) } diff --git a/pkg/promunifi/controller.go b/pkg/promunifi/controller.go new file mode 100644 index 00000000..ef15b9a6 --- /dev/null +++ b/pkg/promunifi/controller.go @@ -0,0 +1,103 @@ +package promunifi + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/unpoller/unifi/v5" +) + +type controller struct { + Info *prometheus.Desc + UptimeSeconds *prometheus.Desc + UpdateAvailable *prometheus.Desc + UpdateDownloaded *prometheus.Desc + AutobackupEnabled *prometheus.Desc + WebRTCSupport *prometheus.Desc + IsCloudConsole *prometheus.Desc + DataRetentionDays *prometheus.Desc + DataRetention5minHours *prometheus.Desc + DataRetentionHourlyHours *prometheus.Desc + DataRetentionDailyHours *prometheus.Desc + DataRetentionMonthlyHours *prometheus.Desc + UnsupportedDeviceCount *prometheus.Desc + InformPort *prometheus.Desc + HTTPSPort *prometheus.Desc + PortalHTTPPort *prometheus.Desc +} + +func descController(ns string) *controller { + labels := []string{"hostname", "site_name", "source"} + infoLabels := []string{"version", "build", "device_type", "console_version", "hostname", "site_name", "source"} + + nd := prometheus.NewDesc + + return &controller{ + Info: nd(ns+"controller_info", "Controller information (always 1)", infoLabels, nil), + UptimeSeconds: nd(ns+"controller_uptime_seconds", "Controller uptime in seconds", labels, nil), + UpdateAvailable: nd(ns+"controller_update_available", "Update available (1/0)", labels, nil), + UpdateDownloaded: nd(ns+"controller_update_downloaded", "Update downloaded (1/0)", labels, nil), + AutobackupEnabled: nd(ns+"controller_autobackup_enabled", "Auto backup enabled (1/0)", labels, nil), + WebRTCSupport: nd(ns+"controller_webrtc_support", "WebRTC supported (1/0)", labels, nil), + IsCloudConsole: nd(ns+"controller_is_cloud_console", "Is cloud console (1/0)", labels, nil), + DataRetentionDays: nd(ns+"controller_data_retention_days", "Data retention in days", labels, nil), + DataRetention5minHours: nd(ns+"controller_data_retention_5min_hours", "5-minute scale retention hours", labels, nil), + DataRetentionHourlyHours: nd(ns+"controller_data_retention_hourly_hours", "Hourly scale retention hours", labels, nil), + DataRetentionDailyHours: nd(ns+"controller_data_retention_daily_hours", "Daily scale retention hours", labels, nil), + DataRetentionMonthlyHours: nd(ns+"controller_data_retention_monthly_hours", "Monthly scale retention hours", labels, nil), + UnsupportedDeviceCount: nd(ns+"controller_unsupported_device_count", "Number of unsupported devices", labels, nil), + InformPort: nd(ns+"controller_inform_port", "Inform port number", labels, nil), + HTTPSPort: nd(ns+"controller_https_port", "HTTPS port number", labels, nil), + PortalHTTPPort: nd(ns+"controller_portal_http_port", "Portal HTTP port number", labels, nil), + } +} + +func (u *promUnifi) exportSysinfo(r report, s *unifi.Sysinfo) { + hostname := s.Hostname + if hostname == "" { + hostname = s.Name + } + if hostname == "" { + hostname = s.SiteName // fallback when API omits both (e.g. remote/cloud) + } + labels := []string{hostname, s.SiteName, s.SourceName} + infoLabels := []string{s.Version, s.Build, s.DeviceType, s.ConsoleVer, hostname, s.SiteName, s.SourceName} + + updateAvail := 0 + if s.UpdateAvail { + updateAvail = 1 + } + updateDown := 0 + if s.UpdateDown { + updateDown = 1 + } + autobackup := 0 + if s.Autobackup { + autobackup = 1 + } + webrtc := 0 + if s.HasWebRTC { + webrtc = 1 + } + cloud := 0 + if s.IsCloud { + cloud = 1 + } + + r.send([]*metric{ + {u.Controller.Info, gauge, 1, infoLabels}, + {u.Controller.UptimeSeconds, gauge, s.Uptime, labels}, + {u.Controller.UpdateAvailable, gauge, updateAvail, labels}, + {u.Controller.UpdateDownloaded, gauge, updateDown, labels}, + {u.Controller.AutobackupEnabled, gauge, autobackup, labels}, + {u.Controller.WebRTCSupport, gauge, webrtc, labels}, + {u.Controller.IsCloudConsole, gauge, cloud, labels}, + {u.Controller.DataRetentionDays, gauge, s.DataRetDays, labels}, + {u.Controller.DataRetention5minHours, gauge, s.DataRet5min, labels}, + {u.Controller.DataRetentionHourlyHours, gauge, s.DataRetHour, labels}, + {u.Controller.DataRetentionDailyHours, gauge, s.DataRetDay, labels}, + {u.Controller.DataRetentionMonthlyHours, gauge, s.DataRetMonth, labels}, + {u.Controller.UnsupportedDeviceCount, gauge, s.Unsupported, labels}, + {u.Controller.InformPort, gauge, s.InformPort, labels}, + {u.Controller.HTTPSPort, gauge, s.HTTPSPort, labels}, + {u.Controller.PortalHTTPPort, gauge, s.PortalPort, labels}, + }) +}