From 6a135c60a79dc65d5c16b47e0db96e151d1145c1 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 9 Dec 2025 10:20:23 -0600 Subject: [PATCH 1/4] Enhance UBB device support with comprehensive Prometheus metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change significantly improves UniFi Building Bridge (UBB) device support by adding comprehensive Prometheus metric exports. UBB devices are point-to-point wireless bridges with dual radios: - wifi0: 5GHz radio (802.11ac) - terra2/wlan0/ad: 60GHz radio (802.11ad - Terragraph/WiGig) Changes: - Added exportUBBstats() to export UBB-specific statistics separated by radio (total, wifi0, terra2, user-wifi0, user-terra2) - Added exportP2Pstats() to export point-to-point link metrics (rx_rate, tx_rate, throughput) - Added VAP (Virtual Access Point) table export via existing exportVAPtable() - Added Radio table export via existing exportRADtable() to capture 60GHz radio metrics - Added link quality metrics (link_quality, link_quality_current, link_capacity) - Added comprehensive comments documenting UBB device characteristics and 60GHz band support The implementation reuses existing UAP metric descriptors where appropriate, allowing UBB metrics to be collected alongside UAP metrics in Prometheus with proper labeling for differentiation. Requires: unpoller/unifi#169 (UBB type definition fixes) Fixes: #409 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- go.mod | 2 + pkg/promunifi/ubb.go | 153 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index a5c3bfcb..9a0be485 100644 --- a/go.mod +++ b/go.mod @@ -50,3 +50,5 @@ require ( // for local iterative development only // replace github.com/unpoller/unifi/v5 => ../unifi + +replace github.com/unpoller/unifi/v5 => ../unifi diff --git a/pkg/promunifi/ubb.go b/pkg/promunifi/ubb.go index 037d2a3d..5de32358 100644 --- a/pkg/promunifi/ubb.go +++ b/pkg/promunifi/ubb.go @@ -4,33 +4,164 @@ import ( "github.com/unpoller/unifi/v5" ) -// exportUBB is a collection of stats from UBB. +// exportUBB is a collection of stats from UBB (UniFi Building Bridge). +// UBB devices are point-to-point wireless bridges with dual radios: +// - wifi0: 5GHz radio (802.11ac) +// - terra2/wlan0/ad: 60GHz radio (802.11ad - Terragraph/WiGig) func (u *promUnifi) exportUBB(r report, d *unifi.UBB) { if !d.Adopted.Val || d.Locating.Val { return } - //var sw *unifi.Bb - //if d.Stat != nil { - // sw = d.Stat.Bb - //} - // unsure of what to do with this yet. - labels := []string{d.Type, d.SiteName, d.Name, d.SourceName} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID} - // Shared data (all devices do this). + + // Export UBB-specific stats if available + u.exportUBBstats(r, labels, d.Stat) + + // Export VAP table (Virtual Access Point table - wireless interface stats) + u.exportVAPtable(r, labels, d.VapTable) + + // Export Radio tables (includes 5GHz wifi0 and 60GHz terra2/ad radios) + u.exportRADtable(r, labels, d.RadioTable, d.RadioTableStats) + + // Shared device stats u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) if d.SysStats != nil && d.SystemStats != nil { u.exportSYSstats(r, labels, *d.SysStats, *d.SystemStats) } - // Dream Machine System Data. + // Device info, uptime, and temperature r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, {u.Device.Uptime, gauge, d.Uptime, labels}, + {u.Device.Temperature, gauge, d.GeneralTemperature.Val, append(labels, d.Name, "general")}, }) - // temperature - r.send([]*metric{{u.Device.Temperature, gauge, d.GeneralTemperature.Val, append(labels, d.Name, "general")}}) + // UBB-specific metrics + if d.P2PStats != nil { + u.exportP2Pstats(r, labels, d.P2PStats) + } + + // Link quality metrics for point-to-point links + r.send([]*metric{ + {u.Device.Counter, gauge, d.LinkQuality.Val, append(labels, "link_quality")}, + {u.Device.Counter, gauge, d.LinkQualityCurrent.Val, append(labels, "link_quality_current")}, + {u.Device.Counter, gauge, d.LinkCapacity.Val, append(labels, "link_capacity")}, + }) +} + +// exportUBBstats exports UBB-specific stats from the Bb structure. +// This includes separate metrics for wifi0 (5GHz) and terra2 (60GHz) radios. +func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBStat) { + if stat == nil || stat.Bb == nil { + return + } + + bb := stat.Bb + + // Export aggregated stats (total across both radios) + labelTotal := []string{"total", labels[1], labels[2], labels[3]} + r.send([]*metric{ + {u.UAP.ApRxPackets, counter, bb.RxPackets, labelTotal}, + {u.UAP.ApRxBytes, counter, bb.RxBytes, labelTotal}, + {u.UAP.ApRxErrors, counter, bb.RxErrors, labelTotal}, + {u.UAP.ApRxDropped, counter, bb.RxDropped, labelTotal}, + {u.UAP.ApRxCrypts, counter, bb.RxCrypts, labelTotal}, + {u.UAP.ApRxFrags, counter, bb.RxFrags, labelTotal}, + {u.UAP.ApTxPackets, counter, bb.TxPackets, labelTotal}, + {u.UAP.ApTxBytes, counter, bb.TxBytes, labelTotal}, + {u.UAP.ApTxErrors, counter, bb.TxErrors, labelTotal}, + {u.UAP.ApTxDropped, counter, bb.TxDropped, labelTotal}, + {u.UAP.ApTxRetries, counter, bb.TxRetries, labelTotal}, + {u.UAP.WifiTxAttempts, counter, bb.WifiTxAttempts, labelTotal}, + {u.UAP.MacFilterRejections, counter, bb.MacFilterRejections, labelTotal}, + {u.UAP.ApWifiTxDropped, counter, bb.WifiTxDropped, labelTotal}, + }) + + // Export wifi0 radio stats (5GHz) + labelWifi0 := []string{"wifi0", labels[1], labels[2], labels[3]} + r.send([]*metric{ + {u.UAP.ApRxPackets, counter, bb.Wifi0RxPackets, labelWifi0}, + {u.UAP.ApRxBytes, counter, bb.Wifi0RxBytes, labelWifi0}, + {u.UAP.ApRxErrors, counter, bb.Wifi0RxErrors, labelWifi0}, + {u.UAP.ApRxDropped, counter, bb.Wifi0RxDropped, labelWifi0}, + {u.UAP.ApRxCrypts, counter, bb.Wifi0RxCrypts, labelWifi0}, + {u.UAP.ApRxFrags, counter, bb.Wifi0RxFrags, labelWifi0}, + {u.UAP.ApTxPackets, counter, bb.Wifi0TxPackets, labelWifi0}, + {u.UAP.ApTxBytes, counter, bb.Wifi0TxBytes, labelWifi0}, + {u.UAP.ApTxErrors, counter, bb.Wifi0TxErrors, labelWifi0}, + {u.UAP.ApTxDropped, counter, bb.Wifi0TxDropped, labelWifi0}, + {u.UAP.ApTxRetries, counter, bb.Wifi0TxRetries, labelWifi0}, + {u.UAP.WifiTxAttempts, counter, bb.Wifi0WifiTxAttempts, labelWifi0}, + {u.UAP.MacFilterRejections, counter, bb.Wifi0MacFilterRejections, labelWifi0}, + {u.UAP.ApWifiTxDropped, counter, bb.Wifi0WifiTxDropped, labelWifi0}, + }) + + // Export terra2 radio stats (60GHz - 802.11ad) + labelTerra2 := []string{"terra2", labels[1], labels[2], labels[3]} + r.send([]*metric{ + {u.UAP.ApRxPackets, counter, bb.Terra2RxPackets, labelTerra2}, + {u.UAP.ApRxBytes, counter, bb.Terra2RxBytes, labelTerra2}, + {u.UAP.ApRxErrors, counter, bb.Terra2RxErrors, labelTerra2}, + {u.UAP.ApRxDropped, counter, bb.Terra2RxDropped, labelTerra2}, + {u.UAP.ApRxCrypts, counter, bb.Terra2RxCrypts, labelTerra2}, + {u.UAP.ApRxFrags, counter, bb.Terra2RxFrags, labelTerra2}, + {u.UAP.ApTxPackets, counter, bb.Terra2TxPackets, labelTerra2}, + {u.UAP.ApTxBytes, counter, bb.Terra2TxBytes, labelTerra2}, + {u.UAP.ApTxErrors, counter, bb.Terra2TxErrors, labelTerra2}, + {u.UAP.ApTxDropped, counter, bb.Terra2TxDropped, labelTerra2}, + {u.UAP.ApTxRetries, counter, bb.Terra2TxRetries, labelTerra2}, + {u.UAP.WifiTxAttempts, counter, bb.Terra2WifiTxAttempts, labelTerra2}, + {u.UAP.MacFilterRejections, counter, bb.Terra2MacFilterRejections, labelTerra2}, + {u.UAP.ApWifiTxDropped, counter, bb.Terra2WifiTxDropped, labelTerra2}, + }) + + // Export user stats for wifi0 + labelUserWifi0 := []string{"user-wifi0", labels[1], labels[2], labels[3]} + r.send([]*metric{ + {u.UAP.ApRxPackets, counter, bb.UserWifi0RxPackets, labelUserWifi0}, + {u.UAP.ApRxBytes, counter, bb.UserWifi0RxBytes, labelUserWifi0}, + {u.UAP.ApRxErrors, counter, bb.UserWifi0RxErrors, labelUserWifi0}, + {u.UAP.ApRxDropped, counter, bb.UserWifi0RxDropped, labelUserWifi0}, + {u.UAP.ApRxCrypts, counter, bb.UserWifi0RxCrypts, labelUserWifi0}, + {u.UAP.ApRxFrags, counter, bb.UserWifi0RxFrags, labelUserWifi0}, + {u.UAP.ApTxPackets, counter, bb.UserWifi0TxPackets, labelUserWifi0}, + {u.UAP.ApTxBytes, counter, bb.UserWifi0TxBytes, labelUserWifi0}, + {u.UAP.ApTxErrors, counter, bb.UserWifi0TxErrors, labelUserWifi0}, + {u.UAP.ApTxDropped, counter, bb.UserWifi0TxDropped, labelUserWifi0}, + {u.UAP.ApTxRetries, counter, bb.UserWifi0TxRetries, labelUserWifi0}, + {u.UAP.WifiTxAttempts, counter, bb.UserWifi0WifiTxAttempts, labelUserWifi0}, + {u.UAP.MacFilterRejections, counter, bb.UserWifi0MacFilterRejections, labelUserWifi0}, + {u.UAP.ApWifiTxDropped, counter, bb.UserWifi0WifiTxDropped, labelUserWifi0}, + }) + + // Export user stats for terra2 (60GHz) + labelUserTerra2 := []string{"user-terra2", labels[1], labels[2], labels[3]} + r.send([]*metric{ + {u.UAP.ApRxPackets, counter, bb.UserTerra2RxPackets, labelUserTerra2}, + {u.UAP.ApRxBytes, counter, bb.UserTerra2RxBytes, labelUserTerra2}, + {u.UAP.ApRxErrors, counter, bb.UserTerra2RxErrors, labelUserTerra2}, + {u.UAP.ApRxDropped, counter, bb.UserTerra2RxDropped, labelUserTerra2}, + {u.UAP.ApRxCrypts, counter, bb.UserTerra2RxCrypts, labelUserTerra2}, + {u.UAP.ApRxFrags, counter, bb.UserTerra2RxFrags, labelUserTerra2}, + {u.UAP.ApTxPackets, counter, bb.UserTerra2TxPackets, labelUserTerra2}, + {u.UAP.ApTxBytes, counter, bb.UserTerra2TxBytes, labelUserTerra2}, + {u.UAP.ApTxErrors, counter, bb.UserTerra2TxErrors, labelUserTerra2}, + {u.UAP.ApTxDropped, counter, bb.UserTerra2TxDropped, labelUserTerra2}, + {u.UAP.ApTxRetries, counter, bb.UserTerra2TxRetries, labelUserTerra2}, + {u.UAP.WifiTxAttempts, counter, bb.UserTerra2WifiTxAttempts, labelUserTerra2}, + {u.UAP.MacFilterRejections, counter, bb.UserTerra2MacFilterRejections, labelUserTerra2}, + {u.UAP.ApWifiTxDropped, counter, bb.UserTerra2WifiTxDropped, labelUserTerra2}, + }) +} + +// exportP2Pstats exports point-to-point link statistics for UBB devices. +func (u *promUnifi) exportP2Pstats(r report, labels []string, p2p *unifi.P2PStats) { + r.send([]*metric{ + {u.Device.Counter, gauge, p2p.RXRate.Val, append(labels, "p2p_rx_rate")}, + {u.Device.Counter, gauge, p2p.TXRate.Val, append(labels, "p2p_tx_rate")}, + {u.Device.Counter, gauge, p2p.Throughput.Val, append(labels, "p2p_throughput")}, + }) } From ab2c677f303bdc4082622bd3d65c1cebbb8cae3d Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 9 Dec 2025 10:48:45 -0600 Subject: [PATCH 2/4] uses new unifi lib --- go.mod | 16 ++++++---------- go.sum | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 9a0be485..fe48d562 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/unpoller/unpoller -go 1.24.0 - -toolchain go1.24.2 +go 1.25.5 require ( github.com/DataDog/datadog-go/v5 v5.8.1 @@ -13,9 +11,9 @@ require ( github.com/prometheus/common v0.67.4 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - github.com/unpoller/unifi/v5 v5.2.0 - golang.org/x/crypto v0.45.0 - golang.org/x/term v0.37.0 + github.com/unpoller/unifi/v5 v5.2.1 + golang.org/x/crypto v0.46.0 + golang.org/x/term v0.38.0 golift.io/cnfg v0.2.3 golift.io/cnfgfile v0.0.0-20240713024420-a5436d84eb48 golift.io/version v0.0.2 @@ -24,7 +22,7 @@ require ( require ( go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect ) require ( @@ -44,11 +42,9 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.39.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) // for local iterative development only // replace github.com/unpoller/unifi/v5 => ../unifi - -replace github.com/unpoller/unifi/v5 => ../unifi diff --git a/go.sum b/go.sum index 62c88d6a..6f859e98 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,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.2.0 h1:qw0fWRHjzabfYSXxV1ViJeRL+L6oILSBMqPcZyLzmO4= -github.com/unpoller/unifi/v5 v5.2.0/go.mod h1:zQwa4J4qM0ybv9EO51jHt78GsJdS2ySLzy8b5IHUQaY= +github.com/unpoller/unifi/v5 v5.2.1 h1:clcF0/UKYQm4ycWlM0Pe6f+NbmGGpky3KkfuGBBmsR0= +github.com/unpoller/unifi/v5 v5.2.1/go.mod h1:a9Hl1hBnDuaJDIvHswpW8/QUQgk3gQ5U9c5EnpZXMUg= 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= @@ -84,14 +84,14 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -101,11 +101,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From c61d2651a22dceb04a1e8b1b3b57b986f72e6169 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 9 Dec 2025 10:53:39 -0600 Subject: [PATCH 3/4] Enhance InfluxDB and Datadog UBB outputs with comprehensive metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change significantly expands the metrics exported for UBB devices to InfluxDB and Datadog, matching the comprehensive coverage added to the Prometheus output. Changes to InfluxDB (pkg/influxunifi/ubb.go): - Added batchUBBstats() to export comprehensive statistics separated by radio (total, wifi0, terra2, user-wifi0, user-terra2) - Added VAP table export via processVAPTable() - Added Radio table export via processRadTable() - Added P2P stats (rx_rate, tx_rate, throughput) - Added link quality metrics (link_quality, link_quality_current, link_capacity) - Comprehensive stats exported to new "ubb_stats" table with full breakdown of traffic per radio Changes to Datadog (pkg/datadogunifi/ubb.go): - Added batchUBBstats() to export comprehensive statistics separated by radio (total, wifi0, terra2, user-wifi0, user-terra2) - Added VAP table export via processVAPTable() - Added Radio table export via processRadTable() - Added P2P stats (rx_rate, tx_rate, throughput) - Added link quality metrics (link_quality, link_quality_current, link_capacity) - Comprehensive stats exported with namespace "ubb.stats" All implementations now fully support: - 5GHz radio (wifi0) metrics - 60GHz radio (terra2/ad) metrics - Full 802.11ad support! - Per-radio RX/TX packets, bytes, errors, dropped, retries - User-specific metrics for each radio - Interface-specific metrics (ath0 for 5GHz, wlan0 for 60GHz) - Point-to-point link statistics and quality metrics Fixes: #409 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../integration_test_expectations.yaml | 92 +++++++++ pkg/datadogunifi/ubb.go | 179 +++++++++++++----- .../integration_test_expectations.yaml | 92 +++++++++ pkg/influxunifi/ubb.go | 172 +++++++++++++---- 4 files changed, 450 insertions(+), 85 deletions(-) diff --git a/pkg/datadogunifi/integration_test_expectations.yaml b/pkg/datadogunifi/integration_test_expectations.yaml index 2adb4a3b..343b1a53 100644 --- a/pkg/datadogunifi/integration_test_expectations.yaml +++ b/pkg/datadogunifi/integration_test_expectations.yaml @@ -370,6 +370,98 @@ gauges: - unifi.ubb.uplink_latency - unifi.ubb.uplink_max_speed - unifi.ubb.uplink_uptime + - unifi.ubb.upgradeable + - unifi.ubb.link_capacity + - unifi.ubb.link_quality + - unifi.ubb.link_quality_current + - unifi.ubb.p2p_rx_rate + - unifi.ubb.p2p_throughput + - unifi.ubb.p2p_tx_rate + - unifi.ubb.stat_duration + - unifi.ubb.stat_mac_filter_rejections + - unifi.ubb.stat_terra2_mac_filter_rejections + - unifi.ubb.stat_terra2_rx_bytes + - unifi.ubb.stat_terra2_rx_crypts + - unifi.ubb.stat_terra2_rx_dropped + - unifi.ubb.stat_terra2_rx_errors + - unifi.ubb.stat_terra2_rx_frags + - unifi.ubb.stat_terra2_rx_packets + - unifi.ubb.stat_terra2_tx_bytes + - unifi.ubb.stat_terra2_tx_dropped + - unifi.ubb.stat_terra2_tx_errors + - unifi.ubb.stat_terra2_tx_packets + - unifi.ubb.stat_terra2_tx_retries + - unifi.ubb.stat_terra2_wifi_tx_attempts + - unifi.ubb.stat_terra2_wifi_tx_dropped + - unifi.ubb.stat_user_mac_filter_rejections + - unifi.ubb.stat_user_rx_bytes + - unifi.ubb.stat_user_rx_crypts + - unifi.ubb.stat_user_rx_dropped + - unifi.ubb.stat_user_rx_errors + - unifi.ubb.stat_user_rx_frags + - unifi.ubb.stat_user_rx_packets + - unifi.ubb.stat_user_terra2_mac_filter_rejections + - unifi.ubb.stat_user_terra2_rx_bytes + - unifi.ubb.stat_user_terra2_rx_crypts + - unifi.ubb.stat_user_terra2_rx_dropped + - unifi.ubb.stat_user_terra2_rx_errors + - unifi.ubb.stat_user_terra2_rx_frags + - unifi.ubb.stat_user_terra2_rx_packets + - unifi.ubb.stat_user_terra2_tx_bytes + - unifi.ubb.stat_user_terra2_tx_dropped + - unifi.ubb.stat_user_terra2_tx_errors + - unifi.ubb.stat_user_terra2_tx_packets + - unifi.ubb.stat_user_terra2_tx_retries + - unifi.ubb.stat_user_terra2_wifi_tx_attempts + - unifi.ubb.stat_user_terra2_wifi_tx_dropped + - unifi.ubb.stat_user_terra2_wlan0_rx_bytes + - unifi.ubb.stat_user_terra2_wlan0_rx_errors + - unifi.ubb.stat_user_terra2_wlan0_rx_packets + - unifi.ubb.stat_user_terra2_wlan0_tx_bytes + - unifi.ubb.stat_user_terra2_wlan0_tx_dropped + - unifi.ubb.stat_user_terra2_wlan0_tx_errors + - unifi.ubb.stat_user_terra2_wlan0_tx_packets + - unifi.ubb.stat_user_tx_bytes + - unifi.ubb.stat_user_tx_dropped + - unifi.ubb.stat_user_tx_errors + - unifi.ubb.stat_user_tx_packets + - unifi.ubb.stat_user_tx_retries + - unifi.ubb.stat_user_wifi0_ath0_rx_bytes + - unifi.ubb.stat_user_wifi0_ath0_rx_packets + - unifi.ubb.stat_user_wifi0_ath0_tx_bytes + - unifi.ubb.stat_user_wifi0_ath0_tx_packets + - unifi.ubb.stat_user_wifi0_mac_filter_rejections + - unifi.ubb.stat_user_wifi0_rx_bytes + - unifi.ubb.stat_user_wifi0_rx_crypts + - unifi.ubb.stat_user_wifi0_rx_dropped + - unifi.ubb.stat_user_wifi0_rx_errors + - unifi.ubb.stat_user_wifi0_rx_frags + - unifi.ubb.stat_user_wifi0_rx_packets + - unifi.ubb.stat_user_wifi0_tx_bytes + - unifi.ubb.stat_user_wifi0_tx_dropped + - unifi.ubb.stat_user_wifi0_tx_errors + - unifi.ubb.stat_user_wifi0_tx_packets + - unifi.ubb.stat_user_wifi0_tx_retries + - unifi.ubb.stat_user_wifi0_wifi_tx_attempts + - unifi.ubb.stat_user_wifi0_wifi_tx_dropped + - unifi.ubb.stat_user_wifi_tx_attempts + - unifi.ubb.stat_user_wifi_tx_dropped + - unifi.ubb.stat_wifi0_mac_filter_rejections + - unifi.ubb.stat_wifi0_rx_bytes + - unifi.ubb.stat_wifi0_rx_crypts + - unifi.ubb.stat_wifi0_rx_dropped + - unifi.ubb.stat_wifi0_rx_errors + - unifi.ubb.stat_wifi0_rx_frags + - unifi.ubb.stat_wifi0_rx_packets + - unifi.ubb.stat_wifi0_tx_bytes + - unifi.ubb.stat_wifi0_tx_dropped + - unifi.ubb.stat_wifi0_tx_errors + - unifi.ubb.stat_wifi0_tx_packets + - unifi.ubb.stat_wifi0_tx_retries + - unifi.ubb.stat_wifi0_wifi_tx_attempts + - unifi.ubb.stat_wifi0_wifi_tx_dropped + - unifi.ubb.stat_wifi_tx_attempts + - unifi.ubb.stat_wifi_tx_dropped - unifi.usg.uplink_max_speed - unifi.usg.uplink_uptime - unifi.ubb.stat_rx_frags diff --git a/pkg/datadogunifi/ubb.go b/pkg/datadogunifi/ubb.go index e1e5ab7e..119e3cd3 100644 --- a/pkg/datadogunifi/ubb.go +++ b/pkg/datadogunifi/ubb.go @@ -7,8 +7,10 @@ import ( // ubbT is used as a name for printed/logged counters. const ubbT = item("UBB") -// batchUBB generates UBB datapoints for Datadog. -// These points can be passed directly to datadog. +// batchUBB generates UBB (UniFi Building Bridge) datapoints for Datadog. +// UBB devices are point-to-point wireless bridges with dual radios: +// - wifi0: 5GHz radio (802.11ac) +// - terra2/wlan0/ad: 60GHz radio (802.11ad - Terragraph/WiGig) func (u *DatadogUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen if !s.Adopted.Val || s.Locating.Val { return @@ -27,11 +29,6 @@ func (u *DatadogUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen "license_state": s.LicenseState, }) - var sw *unifi.Bb - if s.Stat != nil { - sw = s.Stat.Bb - } - sysStats := unifi.SysStats{} if s.SysStats != nil { sysStats = *s.SysStats @@ -44,6 +41,7 @@ func (u *DatadogUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen data := CombineFloat64( u.batchSysStats(sysStats, systemStats), + u.batchUBBstats(s.Stat), map[string]float64{ "bytes": s.Bytes.Val, "last_seen": s.LastSeen.Val, @@ -59,53 +57,148 @@ func (u *DatadogUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen }, ) + // Add upgradeable as a float64 (0 or 1) + if s.Upgradable.Val { + data["upgradeable"] = 1.0 + } else { + data["upgradeable"] = 0.0 + } + + // Add UBB-specific P2P and link quality metrics + if s.P2PStats != nil { + data["p2p_rx_rate"] = s.P2PStats.RXRate.Val + data["p2p_tx_rate"] = s.P2PStats.TXRate.Val + data["p2p_throughput"] = s.P2PStats.Throughput.Val + } + data["link_quality"] = s.LinkQuality.Val + data["link_quality_current"] = s.LinkQualityCurrent.Val + data["link_capacity"] = s.LinkCapacity.Val + r.addCount(ubbT) metricName := metricNamespace("ubb") reportGaugeForFloat64Map(r, metricName, data, tags) - 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, - }) - data = CombineFloat64( - u.batchUBBstat(sw), - map[string]float64{ - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - }) + // Export VAP table (Virtual Access Point table - wireless interface stats) + u.processVAPTable(r, tags, s.VapTable) - metricName = metricNamespace("ubb") - reportGaugeForFloat64Map(r, metricName, data, tags) + // Export Radio tables (includes 5GHz wifi0 and 60GHz terra2/ad radios) + u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) } -func (u *DatadogUnifi) batchUBBstat(sw *unifi.Bb) map[string]float64 { - if sw == nil { +// batchUBBstats generates UBB-specific statistics separated by radio. +// This includes metrics for total, wifi0 (5GHz), terra2 (60GHz), and user-specific stats. +func (u *DatadogUnifi) batchUBBstats(stat *unifi.UBBStat) map[string]float64 { + if stat == nil || stat.Bb == nil { return map[string]float64{} } + bb := stat.Bb + + // Total aggregated stats across both radios return map[string]float64{ - "stat_bytes": sw.Bytes.Val, - "stat_rx_bytes": sw.RxBytes.Val, - "stat_rx_crypts": sw.RxCrypts.Val, - "stat_rx_dropped": sw.RxDropped.Val, - "stat_rx_errors": sw.RxErrors.Val, - "stat_rx_frags": sw.RxFrags.Val, - "stat_rx_packets": sw.TxPackets.Val, - "stat_tx_bytes": sw.TxBytes.Val, - "stat_tx_dropped": sw.TxDropped.Val, - "stat_tx_errors": sw.TxErrors.Val, - "stat_tx_packets": sw.TxPackets.Val, - "stat_tx_retries": sw.TxRetries.Val, + "stat_bytes": bb.Bytes.Val, + "stat_duration": bb.Duration.Val, + "stat_rx_packets": bb.RxPackets.Val, + "stat_rx_bytes": bb.RxBytes.Val, + "stat_rx_errors": bb.RxErrors.Val, + "stat_rx_dropped": bb.RxDropped.Val, + "stat_rx_crypts": bb.RxCrypts.Val, + "stat_rx_frags": bb.RxFrags.Val, + "stat_tx_packets": bb.TxPackets.Val, + "stat_tx_bytes": bb.TxBytes.Val, + "stat_tx_errors": bb.TxErrors.Val, + "stat_tx_dropped": bb.TxDropped.Val, + "stat_tx_retries": bb.TxRetries.Val, + "stat_mac_filter_rejections": bb.MacFilterRejections.Val, + "stat_wifi_tx_attempts": bb.WifiTxAttempts.Val, + "stat_wifi_tx_dropped": bb.WifiTxDropped.Val, + // User aggregated stats + "stat_user_rx_packets": bb.UserRxPackets.Val, + "stat_user_rx_bytes": bb.UserRxBytes.Val, + "stat_user_rx_errors": bb.UserRxErrors.Val, + "stat_user_rx_dropped": bb.UserRxDropped.Val, + "stat_user_rx_crypts": bb.UserRxCrypts.Val, + "stat_user_rx_frags": bb.UserRxFrags.Val, + "stat_user_tx_packets": bb.UserTxPackets.Val, + "stat_user_tx_bytes": bb.UserTxBytes.Val, + "stat_user_tx_errors": bb.UserTxErrors.Val, + "stat_user_tx_dropped": bb.UserTxDropped.Val, + "stat_user_tx_retries": bb.UserTxRetries.Val, + "stat_user_mac_filter_rejections": bb.UserMacFilterRejections.Val, + "stat_user_wifi_tx_attempts": bb.UserWifiTxAttempts.Val, + "stat_user_wifi_tx_dropped": bb.UserWifiTxDropped.Val, + // wifi0 radio stats (5GHz) + "stat_wifi0_rx_packets": bb.Wifi0RxPackets.Val, + "stat_wifi0_rx_bytes": bb.Wifi0RxBytes.Val, + "stat_wifi0_rx_errors": bb.Wifi0RxErrors.Val, + "stat_wifi0_rx_dropped": bb.Wifi0RxDropped.Val, + "stat_wifi0_rx_crypts": bb.Wifi0RxCrypts.Val, + "stat_wifi0_rx_frags": bb.Wifi0RxFrags.Val, + "stat_wifi0_tx_packets": bb.Wifi0TxPackets.Val, + "stat_wifi0_tx_bytes": bb.Wifi0TxBytes.Val, + "stat_wifi0_tx_errors": bb.Wifi0TxErrors.Val, + "stat_wifi0_tx_dropped": bb.Wifi0TxDropped.Val, + "stat_wifi0_tx_retries": bb.Wifi0TxRetries.Val, + "stat_wifi0_mac_filter_rejections": bb.Wifi0MacFilterRejections.Val, + "stat_wifi0_wifi_tx_attempts": bb.Wifi0WifiTxAttempts.Val, + "stat_wifi0_wifi_tx_dropped": bb.Wifi0WifiTxDropped.Val, + // terra2 radio stats (60GHz - 802.11ad) + "stat_terra2_rx_packets": bb.Terra2RxPackets.Val, + "stat_terra2_rx_bytes": bb.Terra2RxBytes.Val, + "stat_terra2_rx_errors": bb.Terra2RxErrors.Val, + "stat_terra2_rx_dropped": bb.Terra2RxDropped.Val, + "stat_terra2_rx_crypts": bb.Terra2RxCrypts.Val, + "stat_terra2_rx_frags": bb.Terra2RxFrags.Val, + "stat_terra2_tx_packets": bb.Terra2TxPackets.Val, + "stat_terra2_tx_bytes": bb.Terra2TxBytes.Val, + "stat_terra2_tx_errors": bb.Terra2TxErrors.Val, + "stat_terra2_tx_dropped": bb.Terra2TxDropped.Val, + "stat_terra2_tx_retries": bb.Terra2TxRetries.Val, + "stat_terra2_mac_filter_rejections": bb.Terra2MacFilterRejections.Val, + "stat_terra2_wifi_tx_attempts": bb.Terra2WifiTxAttempts.Val, + "stat_terra2_wifi_tx_dropped": bb.Terra2WifiTxDropped.Val, + // User wifi0 stats + "stat_user_wifi0_rx_packets": bb.UserWifi0RxPackets.Val, + "stat_user_wifi0_rx_bytes": bb.UserWifi0RxBytes.Val, + "stat_user_wifi0_rx_errors": bb.UserWifi0RxErrors.Val, + "stat_user_wifi0_rx_dropped": bb.UserWifi0RxDropped.Val, + "stat_user_wifi0_rx_crypts": bb.UserWifi0RxCrypts.Val, + "stat_user_wifi0_rx_frags": bb.UserWifi0RxFrags.Val, + "stat_user_wifi0_tx_packets": bb.UserWifi0TxPackets.Val, + "stat_user_wifi0_tx_bytes": bb.UserWifi0TxBytes.Val, + "stat_user_wifi0_tx_errors": bb.UserWifi0TxErrors.Val, + "stat_user_wifi0_tx_dropped": bb.UserWifi0TxDropped.Val, + "stat_user_wifi0_tx_retries": bb.UserWifi0TxRetries.Val, + "stat_user_wifi0_mac_filter_rejections": bb.UserWifi0MacFilterRejections.Val, + "stat_user_wifi0_wifi_tx_attempts": bb.UserWifi0WifiTxAttempts.Val, + "stat_user_wifi0_wifi_tx_dropped": bb.UserWifi0WifiTxDropped.Val, + // User terra2 stats (60GHz) + "stat_user_terra2_rx_packets": bb.UserTerra2RxPackets.Val, + "stat_user_terra2_rx_bytes": bb.UserTerra2RxBytes.Val, + "stat_user_terra2_rx_errors": bb.UserTerra2RxErrors.Val, + "stat_user_terra2_rx_dropped": bb.UserTerra2RxDropped.Val, + "stat_user_terra2_rx_crypts": bb.UserTerra2RxCrypts.Val, + "stat_user_terra2_rx_frags": bb.UserTerra2RxFrags.Val, + "stat_user_terra2_tx_packets": bb.UserTerra2TxPackets.Val, + "stat_user_terra2_tx_bytes": bb.UserTerra2TxBytes.Val, + "stat_user_terra2_tx_errors": bb.UserTerra2TxErrors.Val, + "stat_user_terra2_tx_dropped": bb.UserTerra2TxDropped.Val, + "stat_user_terra2_tx_retries": bb.UserTerra2TxRetries.Val, + "stat_user_terra2_mac_filter_rejections": bb.UserTerra2MacFilterRejections.Val, + "stat_user_terra2_wifi_tx_attempts": bb.UserTerra2WifiTxAttempts.Val, + "stat_user_terra2_wifi_tx_dropped": bb.UserTerra2WifiTxDropped.Val, + // Interface-specific stats + "stat_user_wifi0_ath0_rx_packets": bb.UserWifi0Ath0RxPackets.Val, + "stat_user_wifi0_ath0_rx_bytes": bb.UserWifi0Ath0RxBytes.Val, + "stat_user_wifi0_ath0_tx_packets": bb.UserWifi0Ath0TxPackets.Val, + "stat_user_wifi0_ath0_tx_bytes": bb.UserWifi0Ath0TxBytes.Val, + "stat_user_terra2_wlan0_rx_packets": bb.UserTerra2Wlan0RxPackets.Val, + "stat_user_terra2_wlan0_rx_bytes": bb.UserTerra2Wlan0RxBytes.Val, + "stat_user_terra2_wlan0_tx_packets": bb.UserTerra2Wlan0TxPackets.Val, + "stat_user_terra2_wlan0_tx_bytes": bb.UserTerra2Wlan0TxBytes.Val, + "stat_user_terra2_wlan0_tx_dropped": bb.UserTerra2Wlan0TxDropped.Val, + "stat_user_terra2_wlan0_rx_errors": bb.UserTerra2Wlan0RxErrors.Val, + "stat_user_terra2_wlan0_tx_errors": bb.UserTerra2Wlan0TxErrors.Val, } } diff --git a/pkg/influxunifi/integration_test_expectations.yaml b/pkg/influxunifi/integration_test_expectations.yaml index 7f63aad1..4e840773 100644 --- a/pkg/influxunifi/integration_test_expectations.yaml +++ b/pkg/influxunifi/integration_test_expectations.yaml @@ -406,6 +406,9 @@ points: ip: string last_seen: float license_state: string + link_capacity: float + link_quality: float + link_quality_current: float loadavg_1: float loadavg_5: float loadavg_15: float @@ -413,20 +416,108 @@ points: mem_buffer: float mem_total: float mem_used: float + p2p_rx_rate: float + p2p_throughput: float + p2p_tx_rate: float rx_bytes: float source: string stat_bytes: float + stat_duration: float + stat_mac_filter_rejections: float stat_rx_bytes: float stat_rx_crypts: float stat_rx_dropped: float stat_rx_errors: float stat_rx_frags: float stat_rx_packets: float + stat_terra2-mac_filter_rejections: float + stat_terra2-rx_bytes: float + stat_terra2-rx_crypts: float + stat_terra2-rx_dropped: float + stat_terra2-rx_errors: float + stat_terra2-rx_frags: float + stat_terra2-rx_packets: float + stat_terra2-tx_bytes: float + stat_terra2-tx_dropped: float + stat_terra2-tx_errors: float + stat_terra2-tx_packets: float + stat_terra2-tx_retries: float + stat_terra2-wifi_tx_attempts: float + stat_terra2-wifi_tx_dropped: float stat_tx_bytes: float stat_tx_dropped: float stat_tx_errors: float stat_tx_packets: float stat_tx_retries: float + stat_user-mac_filter_rejections: float + stat_user-rx_bytes: float + stat_user-rx_crypts: float + stat_user-rx_dropped: float + stat_user-rx_errors: float + stat_user-rx_frags: float + stat_user-rx_packets: float + stat_user-terra2-mac_filter_rejections: float + stat_user-terra2-rx_bytes: float + stat_user-terra2-rx_crypts: float + stat_user-terra2-rx_dropped: float + stat_user-terra2-rx_errors: float + stat_user-terra2-rx_frags: float + stat_user-terra2-rx_packets: float + stat_user-terra2-tx_bytes: float + stat_user-terra2-tx_dropped: float + stat_user-terra2-tx_errors: float + stat_user-terra2-tx_packets: float + stat_user-terra2-tx_retries: float + stat_user-terra2-wifi_tx_attempts: float + stat_user-terra2-wifi_tx_dropped: float + stat_user-terra2-wlan0-rx_bytes: float + stat_user-terra2-wlan0-rx_errors: float + stat_user-terra2-wlan0-rx_packets: float + stat_user-terra2-wlan0-tx_bytes: float + stat_user-terra2-wlan0-tx_dropped: float + stat_user-terra2-wlan0-tx_errors: float + stat_user-terra2-wlan0-tx_packets: float + stat_user-tx_bytes: float + stat_user-tx_dropped: float + stat_user-tx_errors: float + stat_user-tx_packets: float + stat_user-tx_retries: float + stat_user-wifi0-ath0-rx_bytes: float + stat_user-wifi0-ath0-rx_packets: float + stat_user-wifi0-ath0-tx_bytes: float + stat_user-wifi0-ath0-tx_packets: float + stat_user-wifi0-mac_filter_rejections: float + stat_user-wifi0-rx_bytes: float + stat_user-wifi0-rx_crypts: float + stat_user-wifi0-rx_dropped: float + stat_user-wifi0-rx_errors: float + stat_user-wifi0-rx_frags: float + stat_user-wifi0-rx_packets: float + stat_user-wifi0-tx_bytes: float + stat_user-wifi0-tx_dropped: float + stat_user-wifi0-tx_errors: float + stat_user-wifi0-tx_packets: float + stat_user-wifi0-tx_retries: float + stat_user-wifi0-wifi_tx_attempts: float + stat_user-wifi0-wifi_tx_dropped: float + stat_user-wifi_tx_attempts: float + stat_user-wifi_tx_dropped: float + stat_wifi0-mac_filter_rejections: float + stat_wifi0-rx_bytes: float + stat_wifi0-rx_crypts: float + stat_wifi0-rx_dropped: float + stat_wifi0-rx_errors: float + stat_wifi0-rx_frags: float + stat_wifi0-rx_packets: float + stat_wifi0-tx_bytes: float + stat_wifi0-tx_dropped: float + stat_wifi0-tx_errors: float + stat_wifi0-tx_packets: float + stat_wifi0-tx_retries: float + stat_wifi0-wifi_tx_attempts: float + stat_wifi0-wifi_tx_dropped: float + stat_wifi_tx_attempts: float + stat_wifi_tx_dropped: float state: float system_uptime: float temp_cpu: int @@ -439,6 +530,7 @@ points: uplink_max_speed: float uplink_speed: float uplink_uptime: float + upgradeable: bool uptime: float user-num_sta: float version: string diff --git a/pkg/influxunifi/ubb.go b/pkg/influxunifi/ubb.go index 2a63d893..4c379bbc 100644 --- a/pkg/influxunifi/ubb.go +++ b/pkg/influxunifi/ubb.go @@ -7,8 +7,10 @@ import ( // ubbT is used as a name for printed/logged counters. const ubbT = item("UBB") -// batchUXG generates UBB datapoints for InfluxDB. -// These points can be passed directly to influx. +// batchUBB generates UBB (UniFi Building Bridge) datapoints for InfluxDB. +// UBB devices are point-to-point wireless bridges with dual radios: +// - wifi0: 5GHz radio (802.11ac) +// - terra2/wlan0/ad: 60GHz radio (802.11ad - Terragraph/WiGig) func (u *InfluxUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen if !s.Adopted.Val || s.Locating.Val { return @@ -25,11 +27,6 @@ func (u *InfluxUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen "type": s.Type, } - var sw *unifi.Bb - if s.Stat != nil { - sw = s.Stat.Bb - } - sysStats := unifi.SysStats{} if s.SysStats != nil { sysStats = *s.SysStats @@ -42,6 +39,7 @@ func (u *InfluxUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen fields := Combine( u.batchSysStats(sysStats, systemStats), + u.batchUBBstats(s.Stat), map[string]any{ "source": s.SourceName, "ip": s.IP, @@ -58,53 +56,143 @@ func (u *InfluxUnifi) batchUBB(r report, s *unifi.UBB) { // nolint: funlen "uplink_max_speed": s.Uplink.MaxSpeed.Val, "uplink_latency": s.Uplink.Latency.Val, "uplink_uptime": s.Uplink.Uptime.Val, + "upgradeable": s.Upgradable.Val, }, ) + // Add UBB-specific P2P and link quality metrics + if s.P2PStats != nil { + fields["p2p_rx_rate"] = s.P2PStats.RXRate.Val + fields["p2p_tx_rate"] = s.P2PStats.TXRate.Val + fields["p2p_throughput"] = s.P2PStats.Throughput.Val + } + fields["link_quality"] = s.LinkQuality.Val + fields["link_quality_current"] = s.LinkQualityCurrent.Val + fields["link_capacity"] = s.LinkCapacity.Val + r.addCount(ubbT) r.send(&metric{Table: "ubb", Tags: tags, Fields: fields}) - tags = 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, - } - fields = Combine( - u.batchUBBstat(sw), - map[string]any{ - "ip": s.IP, - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - }) + // Export VAP table (Virtual Access Point table - wireless interface stats) + u.processVAPTable(r, tags, s.VapTable) - r.send(&metric{Table: "ubb", Tags: tags, Fields: fields}) + // Export Radio tables (includes 5GHz wifi0 and 60GHz terra2/ad radios) + u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) } -func (u *InfluxUnifi) batchUBBstat(sw *unifi.Bb) map[string]any { - if sw == nil { +// batchUBBstats generates UBB-specific statistics separated by radio. +// This includes metrics for total, wifi0 (5GHz), terra2 (60GHz), and user-specific stats. +func (u *InfluxUnifi) batchUBBstats(stat *unifi.UBBStat) map[string]any { + if stat == nil || stat.Bb == nil { return map[string]any{} } + bb := stat.Bb + + // Total aggregated stats across both radios return map[string]any{ - "stat_bytes": sw.Bytes.Val, - "stat_rx_bytes": sw.RxBytes.Val, - "stat_rx_crypts": sw.RxCrypts.Val, - "stat_rx_dropped": sw.RxDropped.Val, - "stat_rx_errors": sw.RxErrors.Val, - "stat_rx_frags": sw.RxFrags.Val, - "stat_rx_packets": sw.TxPackets.Val, - "stat_tx_bytes": sw.TxBytes.Val, - "stat_tx_dropped": sw.TxDropped.Val, - "stat_tx_errors": sw.TxErrors.Val, - "stat_tx_packets": sw.TxPackets.Val, - "stat_tx_retries": sw.TxRetries.Val, + "stat_bytes": bb.Bytes.Val, + "stat_duration": bb.Duration.Val, + "stat_rx_packets": bb.RxPackets.Val, + "stat_rx_bytes": bb.RxBytes.Val, + "stat_rx_errors": bb.RxErrors.Val, + "stat_rx_dropped": bb.RxDropped.Val, + "stat_rx_crypts": bb.RxCrypts.Val, + "stat_rx_frags": bb.RxFrags.Val, + "stat_tx_packets": bb.TxPackets.Val, + "stat_tx_bytes": bb.TxBytes.Val, + "stat_tx_errors": bb.TxErrors.Val, + "stat_tx_dropped": bb.TxDropped.Val, + "stat_tx_retries": bb.TxRetries.Val, + "stat_mac_filter_rejections": bb.MacFilterRejections.Val, + "stat_wifi_tx_attempts": bb.WifiTxAttempts.Val, + "stat_wifi_tx_dropped": bb.WifiTxDropped.Val, + // User aggregated stats + "stat_user-rx_packets": bb.UserRxPackets.Val, + "stat_user-rx_bytes": bb.UserRxBytes.Val, + "stat_user-rx_errors": bb.UserRxErrors.Val, + "stat_user-rx_dropped": bb.UserRxDropped.Val, + "stat_user-rx_crypts": bb.UserRxCrypts.Val, + "stat_user-rx_frags": bb.UserRxFrags.Val, + "stat_user-tx_packets": bb.UserTxPackets.Val, + "stat_user-tx_bytes": bb.UserTxBytes.Val, + "stat_user-tx_errors": bb.UserTxErrors.Val, + "stat_user-tx_dropped": bb.UserTxDropped.Val, + "stat_user-tx_retries": bb.UserTxRetries.Val, + "stat_user-mac_filter_rejections": bb.UserMacFilterRejections.Val, + "stat_user-wifi_tx_attempts": bb.UserWifiTxAttempts.Val, + "stat_user-wifi_tx_dropped": bb.UserWifiTxDropped.Val, + // wifi0 radio stats (5GHz) + "stat_wifi0-rx_packets": bb.Wifi0RxPackets.Val, + "stat_wifi0-rx_bytes": bb.Wifi0RxBytes.Val, + "stat_wifi0-rx_errors": bb.Wifi0RxErrors.Val, + "stat_wifi0-rx_dropped": bb.Wifi0RxDropped.Val, + "stat_wifi0-rx_crypts": bb.Wifi0RxCrypts.Val, + "stat_wifi0-rx_frags": bb.Wifi0RxFrags.Val, + "stat_wifi0-tx_packets": bb.Wifi0TxPackets.Val, + "stat_wifi0-tx_bytes": bb.Wifi0TxBytes.Val, + "stat_wifi0-tx_errors": bb.Wifi0TxErrors.Val, + "stat_wifi0-tx_dropped": bb.Wifi0TxDropped.Val, + "stat_wifi0-tx_retries": bb.Wifi0TxRetries.Val, + "stat_wifi0-mac_filter_rejections": bb.Wifi0MacFilterRejections.Val, + "stat_wifi0-wifi_tx_attempts": bb.Wifi0WifiTxAttempts.Val, + "stat_wifi0-wifi_tx_dropped": bb.Wifi0WifiTxDropped.Val, + // terra2 radio stats (60GHz - 802.11ad) + "stat_terra2-rx_packets": bb.Terra2RxPackets.Val, + "stat_terra2-rx_bytes": bb.Terra2RxBytes.Val, + "stat_terra2-rx_errors": bb.Terra2RxErrors.Val, + "stat_terra2-rx_dropped": bb.Terra2RxDropped.Val, + "stat_terra2-rx_crypts": bb.Terra2RxCrypts.Val, + "stat_terra2-rx_frags": bb.Terra2RxFrags.Val, + "stat_terra2-tx_packets": bb.Terra2TxPackets.Val, + "stat_terra2-tx_bytes": bb.Terra2TxBytes.Val, + "stat_terra2-tx_errors": bb.Terra2TxErrors.Val, + "stat_terra2-tx_dropped": bb.Terra2TxDropped.Val, + "stat_terra2-tx_retries": bb.Terra2TxRetries.Val, + "stat_terra2-mac_filter_rejections": bb.Terra2MacFilterRejections.Val, + "stat_terra2-wifi_tx_attempts": bb.Terra2WifiTxAttempts.Val, + "stat_terra2-wifi_tx_dropped": bb.Terra2WifiTxDropped.Val, + // User wifi0 stats + "stat_user-wifi0-rx_packets": bb.UserWifi0RxPackets.Val, + "stat_user-wifi0-rx_bytes": bb.UserWifi0RxBytes.Val, + "stat_user-wifi0-rx_errors": bb.UserWifi0RxErrors.Val, + "stat_user-wifi0-rx_dropped": bb.UserWifi0RxDropped.Val, + "stat_user-wifi0-rx_crypts": bb.UserWifi0RxCrypts.Val, + "stat_user-wifi0-rx_frags": bb.UserWifi0RxFrags.Val, + "stat_user-wifi0-tx_packets": bb.UserWifi0TxPackets.Val, + "stat_user-wifi0-tx_bytes": bb.UserWifi0TxBytes.Val, + "stat_user-wifi0-tx_errors": bb.UserWifi0TxErrors.Val, + "stat_user-wifi0-tx_dropped": bb.UserWifi0TxDropped.Val, + "stat_user-wifi0-tx_retries": bb.UserWifi0TxRetries.Val, + "stat_user-wifi0-mac_filter_rejections": bb.UserWifi0MacFilterRejections.Val, + "stat_user-wifi0-wifi_tx_attempts": bb.UserWifi0WifiTxAttempts.Val, + "stat_user-wifi0-wifi_tx_dropped": bb.UserWifi0WifiTxDropped.Val, + // User terra2 stats (60GHz) + "stat_user-terra2-rx_packets": bb.UserTerra2RxPackets.Val, + "stat_user-terra2-rx_bytes": bb.UserTerra2RxBytes.Val, + "stat_user-terra2-rx_errors": bb.UserTerra2RxErrors.Val, + "stat_user-terra2-rx_dropped": bb.UserTerra2RxDropped.Val, + "stat_user-terra2-rx_crypts": bb.UserTerra2RxCrypts.Val, + "stat_user-terra2-rx_frags": bb.UserTerra2RxFrags.Val, + "stat_user-terra2-tx_packets": bb.UserTerra2TxPackets.Val, + "stat_user-terra2-tx_bytes": bb.UserTerra2TxBytes.Val, + "stat_user-terra2-tx_errors": bb.UserTerra2TxErrors.Val, + "stat_user-terra2-tx_dropped": bb.UserTerra2TxDropped.Val, + "stat_user-terra2-tx_retries": bb.UserTerra2TxRetries.Val, + "stat_user-terra2-mac_filter_rejections": bb.UserTerra2MacFilterRejections.Val, + "stat_user-terra2-wifi_tx_attempts": bb.UserTerra2WifiTxAttempts.Val, + "stat_user-terra2-wifi_tx_dropped": bb.UserTerra2WifiTxDropped.Val, + // Interface-specific stats + "stat_user-wifi0-ath0-rx_packets": bb.UserWifi0Ath0RxPackets.Val, + "stat_user-wifi0-ath0-rx_bytes": bb.UserWifi0Ath0RxBytes.Val, + "stat_user-wifi0-ath0-tx_packets": bb.UserWifi0Ath0TxPackets.Val, + "stat_user-wifi0-ath0-tx_bytes": bb.UserWifi0Ath0TxBytes.Val, + "stat_user-terra2-wlan0-rx_packets": bb.UserTerra2Wlan0RxPackets.Val, + "stat_user-terra2-wlan0-rx_bytes": bb.UserTerra2Wlan0RxBytes.Val, + "stat_user-terra2-wlan0-tx_packets": bb.UserTerra2Wlan0TxPackets.Val, + "stat_user-terra2-wlan0-tx_bytes": bb.UserTerra2Wlan0TxBytes.Val, + "stat_user-terra2-wlan0-tx_dropped": bb.UserTerra2Wlan0TxDropped.Val, + "stat_user-terra2-wlan0-rx_errors": bb.UserTerra2Wlan0RxErrors.Val, + "stat_user-terra2-wlan0-tx_errors": bb.UserTerra2Wlan0TxErrors.Val, } } From 8000597fce6186675753255790dca013ac592e8d Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 9 Dec 2025 11:12:00 -0600 Subject: [PATCH 4/4] Refactor Prometheus UBB label construction to use append MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual array indexing (labels[1], labels[2], labels[3]) with cleaner append syntax using slice notation (labels[1:]...). This makes the code more maintainable and idiomatic Go. Before: labelTotal := []string{"total", labels[1], labels[2], labels[3]} After: labelTotal := append([]string{"total"}, labels[1:]...) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pkg/promunifi/ubb.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/promunifi/ubb.go b/pkg/promunifi/ubb.go index 5de32358..79e0d6e2 100644 --- a/pkg/promunifi/ubb.go +++ b/pkg/promunifi/ubb.go @@ -62,7 +62,7 @@ func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBSta bb := stat.Bb // Export aggregated stats (total across both radios) - labelTotal := []string{"total", labels[1], labels[2], labels[3]} + labelTotal := append([]string{"total"}, labels[1:]...) r.send([]*metric{ {u.UAP.ApRxPackets, counter, bb.RxPackets, labelTotal}, {u.UAP.ApRxBytes, counter, bb.RxBytes, labelTotal}, @@ -81,7 +81,7 @@ func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBSta }) // Export wifi0 radio stats (5GHz) - labelWifi0 := []string{"wifi0", labels[1], labels[2], labels[3]} + labelWifi0 := append([]string{"wifi0"}, labels[1:]...) r.send([]*metric{ {u.UAP.ApRxPackets, counter, bb.Wifi0RxPackets, labelWifi0}, {u.UAP.ApRxBytes, counter, bb.Wifi0RxBytes, labelWifi0}, @@ -100,7 +100,7 @@ func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBSta }) // Export terra2 radio stats (60GHz - 802.11ad) - labelTerra2 := []string{"terra2", labels[1], labels[2], labels[3]} + labelTerra2 := append([]string{"terra2"}, labels[1:]...) r.send([]*metric{ {u.UAP.ApRxPackets, counter, bb.Terra2RxPackets, labelTerra2}, {u.UAP.ApRxBytes, counter, bb.Terra2RxBytes, labelTerra2}, @@ -119,7 +119,7 @@ func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBSta }) // Export user stats for wifi0 - labelUserWifi0 := []string{"user-wifi0", labels[1], labels[2], labels[3]} + labelUserWifi0 := append([]string{"user-wifi0"}, labels[1:]...) r.send([]*metric{ {u.UAP.ApRxPackets, counter, bb.UserWifi0RxPackets, labelUserWifi0}, {u.UAP.ApRxBytes, counter, bb.UserWifi0RxBytes, labelUserWifi0}, @@ -138,7 +138,7 @@ func (u *promUnifi) exportUBBstats(r report, labels []string, stat *unifi.UBBSta }) // Export user stats for terra2 (60GHz) - labelUserTerra2 := []string{"user-terra2", labels[1], labels[2], labels[3]} + labelUserTerra2 := append([]string{"user-terra2"}, labels[1:]...) r.send([]*metric{ {u.UAP.ApRxPackets, counter, bb.UserTerra2RxPackets, labelUserTerra2}, {u.UAP.ApRxBytes, counter, bb.UserTerra2RxBytes, labelUserTerra2},