From b1a8d60460ac033f669dd83efb78fb1c4dc3f822 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 9 May 2026 09:23:41 -0500 Subject: [PATCH] feat: add UAP uplink metrics and Prometheus parity for USW/UBB/UDB (closes #988) Exposes the uplink medium (wire vs wireless) and link speed for UniFi access points so users can detect when an AP downgrades from gigabit to fast ethernet, which was the original ask in #988. UAPs previously had zero uplink coverage in any output plugin; now influxunifi, datadogunifi, and promunifi all report uplink_type, uplink_speed, uplink_max_speed, and related fields. Also brings Prometheus to parity with Influx/Datadog by emitting uplink metrics for USW, UBB, and UDB devices (previously only USG/UDM/UXG had them in promunifi). A new exportDeviceUplink helper in promunifi/usg.go reuses the existing unpoller_device_uplink_* descriptors to avoid descriptor collision (per c48b9917). Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/datadogunifi/uap.go | 26 +++++++++++++++++--------- pkg/influxunifi/uap.go | 8 ++++++++ pkg/promunifi/uap.go | 6 ++++++ pkg/promunifi/ubb.go | 4 ++++ pkg/promunifi/udb.go | 1 + pkg/promunifi/usg.go | 19 +++++++++++++++++++ pkg/promunifi/usw.go | 1 + 7 files changed, 56 insertions(+), 9 deletions(-) diff --git a/pkg/datadogunifi/uap.go b/pkg/datadogunifi/uap.go index fb2166da..ea9bdd9d 100644 --- a/pkg/datadogunifi/uap.go +++ b/pkg/datadogunifi/uap.go @@ -45,15 +45,16 @@ func (u *DatadogUnifi) batchRogueAP(r report, s *unifi.RogueAP) { // These points can be passed directly to datadog. func (u *DatadogUnifi) batchUAP(r report, s *unifi.UAP) { tags := cleanTags(map[string]string{ - "mac": s.Mac, - "site_name": s.SiteName, - "source": s.SourceName, - "name": s.Name, - "version": s.Version, - "model": s.Model, - "serial": s.Serial, - "type": s.Type, - "ip": s.IP, + "mac": s.Mac, + "site_name": s.SiteName, + "source": s.SourceName, + "name": s.Name, + "version": s.Version, + "model": s.Model, + "serial": s.Serial, + "type": s.Type, + "ip": s.IP, + "uplink_type": s.Uplink.Type, }) data := CombineFloat64( u.processUAPstats(s.Stat.Ap), @@ -70,6 +71,13 @@ func (u *DatadogUnifi) batchUAP(r report, s *unifi.UAP) { data["upgradeable"] = s.Upgradable.Float64() data["adopted"] = s.Adopted.Float64() data["locating"] = s.Locating.Float64() + data["uplink_speed"] = s.Uplink.Speed.Val + data["uplink_max_speed"] = s.Uplink.MaxSpeed.Val + data["uplink_up"] = s.Uplink.Up.Float64() + data["uplink_full_duplex"] = s.Uplink.FullDuplex.Float64() + data["uplink_num_port"] = float64(s.Uplink.NumPort) + data["uplink_rx_bytes"] = s.Uplink.RxBytes.Val + data["uplink_tx_bytes"] = s.Uplink.TxBytes.Val r.addCount(uapT) diff --git a/pkg/influxunifi/uap.go b/pkg/influxunifi/uap.go index 22b63ad2..1673c17c 100644 --- a/pkg/influxunifi/uap.go +++ b/pkg/influxunifi/uap.go @@ -67,6 +67,14 @@ func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { fields["guest-num_sta"] = int(s.GuestNumSta.Val) fields["num_sta"] = s.NumSta.Val fields["upgradeable"] = s.Upgradable.Val + fields["uplink_type"] = s.Uplink.Type + fields["uplink_speed"] = s.Uplink.Speed.Val + fields["uplink_max_speed"] = s.Uplink.MaxSpeed.Val + fields["uplink_up"] = s.Uplink.Up.Val + fields["uplink_full_duplex"] = s.Uplink.FullDuplex.Val + fields["uplink_num_port"] = s.Uplink.NumPort + fields["uplink_rx_bytes"] = s.Uplink.RxBytes.Val + fields["uplink_tx_bytes"] = s.Uplink.TxBytes.Val r.addCount(uapT) r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) diff --git a/pkg/promunifi/uap.go b/pkg/promunifi/uap.go index 0b657246..8f7ba328 100644 --- a/pkg/promunifi/uap.go +++ b/pkg/promunifi/uap.go @@ -230,10 +230,16 @@ func (u *promUnifi) exportUAP(r report, d *unifi.UAP) { u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) u.exportRADtable(r, labels, d.RadioTable, d.RadioTableStats) + // UAP uplink metrics. The uplink "type" (e.g. "wire" / "wireless") + // is encoded as the first label, matching the convention used by + // USG/USW/UBB/UDB so dashboards can group by port. + labelUL := append([]string{d.Uplink.Type}, labels[1:]...) r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(baseLabels, infoLabels...)}, {u.Device.Uptime, gauge, d.Uptime, labels}, {u.Device.Upgradeable, gauge, d.Upgradable.Val, labels}, + {u.USG.UplinkSpeed, gauge, d.Uplink.Speed, labelUL}, + {u.USG.UplinkMaxSpeed, gauge, d.Uplink.MaxSpeed, labelUL}, }) }) } diff --git a/pkg/promunifi/ubb.go b/pkg/promunifi/ubb.go index 5c98cc95..9afb8135 100644 --- a/pkg/promunifi/ubb.go +++ b/pkg/promunifi/ubb.go @@ -37,6 +37,10 @@ func (u *promUnifi) exportUBB(r report, d *unifi.UBB) { u.exportSYSstats(r, labels, *d.SysStats, *d.SystemStats) } + if d.Uplink != nil { + u.exportDeviceUplink(r, labels, *d.Uplink) + } + // Device info, uptime, and temperature r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(baseLabels, infoLabels...)}, diff --git a/pkg/promunifi/udb.go b/pkg/promunifi/udb.go index 34ee8522..92b5b8c6 100644 --- a/pkg/promunifi/udb.go +++ b/pkg/promunifi/udb.go @@ -31,6 +31,7 @@ func (u *promUnifi) exportUDB(r report, d *unifi.UDB) { u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) + u.exportDeviceUplink(r, labels, d.Uplink) r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(baseLabels, infoLabels...)}, diff --git a/pkg/promunifi/usg.go b/pkg/promunifi/usg.go index d72e09f3..1e9a3d63 100644 --- a/pkg/promunifi/usg.go +++ b/pkg/promunifi/usg.go @@ -117,6 +117,25 @@ func (u *promUnifi) exportUSG(r report, d *unifi.USG) { }) } +// exportDeviceUplink emits the shared uplink metrics for any device that +// carries a unifi.Uplink (USW/UBB/UDB). The uplink port name is used as the +// first label, matching the convention used by exportUSGstats. +func (u *promUnifi) exportDeviceUplink(r report, labels []string, ul unifi.Uplink) { + port := ul.Name + if port == "" { + port = "uplink" + } + + labelUL := []string{port, labels[1], labels[2], labels[3], labels[4]} + + r.send([]*metric{ + {u.USG.UplinkLatency, gauge, ul.Latency.Val / 1000, labelUL}, + {u.USG.UplinkSpeed, gauge, ul.Speed, labelUL}, + {u.USG.UplinkMaxSpeed, gauge, ul.MaxSpeed, labelUL}, + {u.USG.UplinkUptime, gauge, ul.Uptime, labelUL}, + }) +} + // Gateway Stats. func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st unifi.SpeedtestStatus, ul unifi.Uplink) { var sourceInterface string diff --git a/pkg/promunifi/usw.go b/pkg/promunifi/usw.go index abdb7da2..7bf655c8 100644 --- a/pkg/promunifi/usw.go +++ b/pkg/promunifi/usw.go @@ -129,6 +129,7 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) { u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) + u.exportDeviceUplink(r, labels, d.Uplink) r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(baseLabels, infoLabels...)}, {u.Device.Uptime, gauge, d.Uptime, labels},