Merge pull request #940 from brngates98/feat/sysinfo-metrics

feat: add controller sysinfo metrics (unpoller#927)
This commit is contained in:
Cody Lee
2026-01-31 19:27:09 -06:00
committed by GitHub
8 changed files with 140 additions and 5 deletions

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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
}

View File

@@ -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)
}

103
pkg/promunifi/controller.go Normal file
View File

@@ -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},
})
}