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) <noreply@anthropic.com>
This commit is contained in:
Cody Lee
2026-05-09 09:23:41 -05:00
co-authored by Claude Opus 4.7
parent cf56aa9c58
commit b1a8d60460
7 changed files with 56 additions and 9 deletions
+17 -9
View File
@@ -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)
+8
View File
@@ -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})
+6
View File
@@ -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},
})
})
}
+4
View File
@@ -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...)},
+1
View File
@@ -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...)},
+19
View File
@@ -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
+1
View File
@@ -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},