Files
unpoller/pkg/datadogunifi/uap.go
Cody Lee 8c7f1cb854 fix: remove age==0 guard that silently dropped all rogue AP metrics (#972)
save_rogue = true collected data from the controller but never wrote
any of it to the output backends. All three exporters (InfluxDB, Datadog,
Prometheus) had the same guard:

    if s.Age.Val == 0 { return }

The intent was to drop stale entries, but the logic is inverted: Age==0
means brand-new or (more commonly) that the UniFi controller did not
include an "age" field in the JSON response, causing FlexInt to default
to 0. This silently discarded every rogue AP record.

Remove the guard entirely. The data was just fetched on-demand from the
controller; if the user opted in to save_rogue, they want all of it.

Fixes #405

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:53:07 -05:00

236 lines
7.9 KiB
Go

package datadogunifi
import (
"strings"
"github.com/unpoller/unifi/v5"
)
// uapT is used as a name for printed/logged counters.
const uapT = item("UAP")
// batchRogueAP generates metric points for neighboring access points.
func (u *DatadogUnifi) batchRogueAP(r report, s *unifi.RogueAP) {
tags := cleanTags(map[string]string{
"security": s.Security,
"oui": s.Oui,
"band": s.Band,
"mac": s.Bssid,
"ap_mac": s.ApMac,
"radio": s.Radio,
"radio_name": s.RadioName,
"site_name": s.SiteName,
"name": s.Essid,
"source": s.SourceName,
})
data := map[string]float64{
"age": s.Age.Val,
"bw": s.Bw.Val,
"center_freq": s.CenterFreq.Val,
"channel": float64(s.Channel),
"freq": s.Freq.Val,
"noise": s.Noise.Val,
"rssi": s.Rssi.Val,
"rssi_age": s.RssiAge.Val,
"signal": s.Signal.Val,
}
metricName := metricNamespace("uap_rogue")
reportGaugeForFloat64Map(r, metricName, data, tags)
}
// batchUAP generates Wireless-Access-Point datapoints for Datadog.
// 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,
})
data := CombineFloat64(
u.processUAPstats(s.Stat.Ap),
u.batchSysStats(s.SysStats, s.SystemStats),
)
data["bytes"] = s.Bytes.Val
data["last_seen"] = s.LastSeen.Val
data["rx_bytes"] = s.RxBytes.Val
data["tx_bytes"] = s.TxBytes.Val
data["uptime"] = s.Uptime.Val
data["user_num_sta"] = s.UserNumSta.Val
data["guest_num_sta"] = s.GuestNumSta.Val
data["num_sta"] = s.NumSta.Val
data["upgradeable"] = s.Upgradable.Float64()
data["adopted"] = s.Adopted.Float64()
data["locating"] = s.Locating.Float64()
r.addCount(uapT)
metricName := metricNamespace("uap")
reportGaugeForFloat64Map(r, metricName, data, tags)
u.processVAPTable(r, tags, s.VapTable)
u.batchPortTable(r, tags, s.PortTable)
}
func (u *DatadogUnifi) processUAPstats(ap *unifi.Ap) map[string]float64 {
if ap == nil {
return map[string]float64{}
}
// Accumulative Statistics.
return map[string]float64{
"stat_user-rx_packets": ap.UserRxPackets.Val,
"stat_guest-rx_packets": ap.GuestRxPackets.Val,
"stat_rx_packets": ap.RxPackets.Val,
"stat_user-rx_bytes": ap.UserRxBytes.Val,
"stat_guest-rx_bytes": ap.GuestRxBytes.Val,
"stat_rx_bytes": ap.RxBytes.Val,
"stat_user-rx_errors": ap.UserRxErrors.Val,
"stat_guest-rx_errors": ap.GuestRxErrors.Val,
"stat_rx_errors": ap.RxErrors.Val,
"stat_user-rx_dropped": ap.UserRxDropped.Val,
"stat_guest-rx_dropped": ap.GuestRxDropped.Val,
"stat_rx_dropped": ap.RxDropped.Val,
"stat_user-rx_crypts": ap.UserRxCrypts.Val,
"stat_guest-rx_crypts": ap.GuestRxCrypts.Val,
"stat_rx_crypts": ap.RxCrypts.Val,
"stat_user-rx_frags": ap.UserRxFrags.Val,
"stat_guest-rx_frags": ap.GuestRxFrags.Val,
"stat_rx_frags": ap.RxFrags.Val,
"stat_user-tx_packets": ap.UserTxPackets.Val,
"stat_guest-tx_packets": ap.GuestTxPackets.Val,
"stat_tx_packets": ap.TxPackets.Val,
"stat_user-tx_bytes": ap.UserTxBytes.Val,
"stat_guest-tx_bytes": ap.GuestTxBytes.Val,
"stat_tx_bytes": ap.TxBytes.Val,
"stat_user-tx_errors": ap.UserTxErrors.Val,
"stat_guest-tx_errors": ap.GuestTxErrors.Val,
"stat_tx_errors": ap.TxErrors.Val,
"stat_user-tx_dropped": ap.UserTxDropped.Val,
"stat_guest-tx_dropped": ap.GuestTxDropped.Val,
"stat_tx_dropped": ap.TxDropped.Val,
"stat_user-tx_retries": ap.UserTxRetries.Val,
"stat_guest-tx_retries": ap.GuestTxRetries.Val,
}
}
// processVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices.
func (u *DatadogUnifi) processVAPTable(r report, t map[string]string, vt unifi.VapTable) { // nolint: funlen
for _, s := range vt {
tags := map[string]string{
"device_name": t["name"],
"site_name": t["site_name"],
"source": t["source"],
"ap_mac": s.ApMac,
"bssid": s.Bssid,
"id": s.ID,
"name": s.Name,
"radio_name": s.RadioName,
"radio": s.Radio,
"essid": s.Essid,
"site_id": s.SiteID,
"usage": s.Usage,
"state": s.State,
"is_guest": s.IsGuest.Txt,
}
data := map[string]float64{
"ccq": float64(s.Ccq),
"mac_filter_rejections": float64(s.MacFilterRejections),
"num_satisfaction_sta": s.NumSatisfactionSta.Val,
"avg_client_signal": s.AvgClientSignal.Val,
"satisfaction": s.Satisfaction.Val,
"satisfaction_now": s.SatisfactionNow.Val,
"num_sta": float64(s.NumSta),
"channel": s.Channel.Val,
"rx_bytes": s.RxBytes.Val,
"rx_crypts": s.RxCrypts.Val,
"rx_dropped": s.RxDropped.Val,
"rx_errors": s.RxErrors.Val,
"rx_frags": s.RxFrags.Val,
"rx_nwids": s.RxNwids.Val,
"rx_packets": s.RxPackets.Val,
"tx_bytes": s.TxBytes.Val,
"tx_dropped": s.TxDropped.Val,
"tx_errors": s.TxErrors.Val,
"tx_packets": s.TxPackets.Val,
"tx_power": s.TxPower.Val,
"tx_retries": s.TxRetries.Val,
"tx_combined_retries": s.TxCombinedRetries.Val,
"tx_data_mpdu_bytes": s.TxDataMpduBytes.Val,
"tx_rts_retries": s.TxRtsRetries.Val,
"tx_success": s.TxSuccess.Val,
"tx_total": s.TxTotal.Val,
"tx_tcp_goodbytes": s.TxTCPStats.Goodbytes.Val,
"tx_tcp_lat_avg": s.TxTCPStats.LatAvg.Val,
"tx_tcp_lat_max": s.TxTCPStats.LatMax.Val,
"tx_tcp_lat_min": s.TxTCPStats.LatMin.Val,
"rx_tcp_goodbytes": s.RxTCPStats.Goodbytes.Val,
"rx_tcp_lat_avg": s.RxTCPStats.LatAvg.Val,
"rx_tcp_lat_max": s.RxTCPStats.LatMax.Val,
"rx_tcp_lat_min": s.RxTCPStats.LatMin.Val,
"wifi_tx_latency_mov_avg": s.WifiTxLatencyMov.Avg.Val,
"wifi_tx_latency_mov_max": s.WifiTxLatencyMov.Max.Val,
"wifi_tx_latency_mov_min": s.WifiTxLatencyMov.Min.Val,
"wifi_tx_latency_mov_total": s.WifiTxLatencyMov.Total.Val,
"wifi_tx_latency_mov_cuont": s.WifiTxLatencyMov.TotalCount.Val,
}
metricName := metricNamespace("uap_vaps")
reportGaugeForFloat64Map(r, metricName, data, tags)
}
}
func (u *DatadogUnifi) processRadTable(r report, t map[string]string, rt unifi.RadioTable, rts unifi.RadioTableStats) {
for _, p := range rt {
tags := map[string]string{
"device_name": t["name"],
"site_name": t["site_name"],
"source": t["source"],
"channel": p.Channel.Txt,
"radio": p.Radio,
"ht": p.Ht.Txt,
}
data := map[string]float64{
"current_antenna_gain": p.CurrentAntennaGain.Val,
"max_txpower": p.MaxTxpower.Val,
"min_txpower": p.MinTxpower.Val,
"nss": p.Nss.Val,
"radio_caps": p.RadioCaps.Val,
}
for _, t := range rts {
if strings.EqualFold(t.Name, p.Name) {
data["ast_be_xmit"] = t.AstBeXmit.Val
data["channel"] = t.Channel.Val
data["cu_self_rx"] = t.CuSelfRx.Val
data["cu_self_tx"] = t.CuSelfTx.Val
data["cu_total"] = t.CuTotal.Val
data["ext_channel"] = t.Extchannel.Val
data["gain"] = t.Gain.Val
data["guest_num_sta"] = t.GuestNumSta.Val
data["num_sta"] = t.NumSta.Val
data["tx_packets"] = t.TxPackets.Val
data["tx_power"] = t.TxPower.Val
data["tx_retries"] = t.TxRetries.Val
data["user_num_sta"] = t.UserNumSta.Val
break
}
}
metricName := metricNamespace("uap_radios")
reportGaugeForFloat64Map(r, metricName, data, tags)
}
}