mirror of
https://github.com/unpoller/unpoller.git
synced 2026-10-21 10:16:07 +02:00
Merge pull request #1101 from unpoller/feat/lte-failover-status
feat: export live U-LTE failover status
This commit is contained in:
@@ -174,6 +174,13 @@ gauges:
|
||||
- unifi.uap.uplink_up
|
||||
- unifi.uap.uptime
|
||||
- unifi.uap.user_num_sta
|
||||
- unifi.uap_lte.connected
|
||||
- unifi.uap_lte.failover
|
||||
- unifi.uap_lte.rsrp
|
||||
- unifi.uap_lte.rsrq
|
||||
- unifi.uap_lte.rssi
|
||||
- unifi.uap_lte.rx_chan
|
||||
- unifi.uap_lte.tx_chan
|
||||
- unifi.uap_radios.ast_be_xmit
|
||||
- unifi.uap_radios.channel
|
||||
- unifi.uap_radios.cu_self_rx
|
||||
|
||||
@@ -84,6 +84,7 @@ func (u *DatadogUnifi) batchUAP(r report, s *unifi.UAP) {
|
||||
metricName := metricNamespace("uap")
|
||||
|
||||
reportGaugeForFloat64Map(r, metricName, data, tags)
|
||||
u.batchLTE(r, s)
|
||||
|
||||
u.processVAPTable(r, tags, s.VapTable)
|
||||
u.batchPortTable(r, tags, s.PortTable)
|
||||
@@ -131,6 +132,49 @@ func (u *DatadogUnifi) processUAPstats(ap *unifi.Ap) map[string]float64 {
|
||||
}
|
||||
}
|
||||
|
||||
// batchLTE writes live cellular backup status for U-LTE and U-LTE-Pro.
|
||||
// Those modules are reported as access points; wired WAN ports do not carry this data.
|
||||
func (u *DatadogUnifi) batchLTE(r report, s *unifi.UAP) {
|
||||
if !hasLTEStatus(s) {
|
||||
return
|
||||
}
|
||||
|
||||
tags := cleanTags(map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"model": s.Model,
|
||||
"type": s.Type,
|
||||
"state": s.LteState.Txt,
|
||||
"failover_mode": s.LteFailoverMode,
|
||||
"signal": s.LteSignal,
|
||||
"rat": s.LteRat,
|
||||
"mode": s.LteMode,
|
||||
"band": s.LteBand,
|
||||
"pdp_type": s.LtePdpType,
|
||||
"operator": s.LteNetworkOperator,
|
||||
})
|
||||
data := map[string]float64{
|
||||
"connected": s.LteConnected.Float64(),
|
||||
"failover": s.LteFailover.Float64(),
|
||||
"rssi": s.LteRssi.Val,
|
||||
"rsrp": s.LteRsrp.Val,
|
||||
"rsrq": s.LteRsrq.Val,
|
||||
"rx_chan": s.LteRxChannel.Val,
|
||||
"tx_chan": s.LteTxChannel.Val,
|
||||
}
|
||||
|
||||
reportGaugeForFloat64Map(r, metricNamespace("uap_lte"), data, tags)
|
||||
}
|
||||
|
||||
// hasLTEStatus reports whether this access point is a cellular backup module.
|
||||
// U-LTE and U-LTE-Pro are typed as UAPs; their live modem status arrives as lte_* fields.
|
||||
func hasLTEStatus(d *unifi.UAP) bool {
|
||||
return d.LteState.Txt != "" || d.LteFailoverMode != "" || d.LteRat != "" ||
|
||||
d.LteNetworkOperator != "" || d.LteSignal != "" || d.LteConnected.Txt != ""
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -297,6 +297,30 @@ points:
|
||||
uplink_up: bool
|
||||
uptime: float
|
||||
user-num_sta: int
|
||||
uap_lte:
|
||||
tags:
|
||||
- band
|
||||
- failover_mode
|
||||
- mac
|
||||
- mode
|
||||
- model
|
||||
- name
|
||||
- operator
|
||||
- pdp_type
|
||||
- rat
|
||||
- signal
|
||||
- site_name
|
||||
- source
|
||||
- state
|
||||
- type
|
||||
fields:
|
||||
connected: bool
|
||||
failover: bool
|
||||
rsrp: float
|
||||
rsrq: float
|
||||
rssi: float
|
||||
rx_chan: float
|
||||
tx_chan: float
|
||||
uap_radios:
|
||||
tags:
|
||||
- channel
|
||||
|
||||
@@ -78,6 +78,7 @@ func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) {
|
||||
|
||||
r.addCount(uapT)
|
||||
r.send(&metric{Table: "uap", Tags: tags, Fields: fields})
|
||||
u.batchLTE(r, s)
|
||||
u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats)
|
||||
u.processVAPTable(r, tags, s.VapTable)
|
||||
u.batchPortTable(r, tags, s.PortTable)
|
||||
@@ -125,6 +126,50 @@ func (u *InfluxUnifi) processUAPstats(ap *unifi.Ap) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// batchLTE writes live cellular backup status for U-LTE and U-LTE-Pro.
|
||||
// Those modules are reported as access points; wired WAN ports do not carry this data.
|
||||
func (u *InfluxUnifi) batchLTE(r report, s *unifi.UAP) {
|
||||
if !hasLTEStatus(s) {
|
||||
return
|
||||
}
|
||||
|
||||
r.send(&metric{
|
||||
Table: "uap_lte",
|
||||
Tags: map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"model": s.Model,
|
||||
"type": s.Type,
|
||||
"state": s.LteState.Txt,
|
||||
"failover_mode": s.LteFailoverMode,
|
||||
"signal": s.LteSignal,
|
||||
"rat": s.LteRat,
|
||||
"mode": s.LteMode,
|
||||
"band": s.LteBand,
|
||||
"pdp_type": s.LtePdpType,
|
||||
"operator": s.LteNetworkOperator,
|
||||
},
|
||||
Fields: map[string]any{
|
||||
"connected": s.LteConnected.Val,
|
||||
"failover": s.LteFailover.Val,
|
||||
"rssi": s.LteRssi.Val,
|
||||
"rsrp": s.LteRsrp.Val,
|
||||
"rsrq": s.LteRsrq.Val,
|
||||
"rx_chan": s.LteRxChannel.Val,
|
||||
"tx_chan": s.LteTxChannel.Val,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// hasLTEStatus reports whether this access point is a cellular backup module.
|
||||
// U-LTE and U-LTE-Pro are typed as UAPs; their live modem status arrives as lte_* fields.
|
||||
func hasLTEStatus(d *unifi.UAP) bool {
|
||||
return d.LteState.Txt != "" || d.LteFailoverMode != "" || d.LteRat != "" ||
|
||||
d.LteNetworkOperator != "" || d.LteSignal != "" || d.LteConnected.Txt != ""
|
||||
}
|
||||
|
||||
// radioBand maps a UniFi radio identifier to its frequency band in GHz.
|
||||
// UniFi reports the radio as ng (2.4 GHz), na (5 GHz) or 6e (6 GHz); the band
|
||||
// is not carried as its own field on the radio/VAP structs, so it is derived
|
||||
|
||||
@@ -47,6 +47,8 @@ func (u *OtelOutput) exportUAP(ctx context.Context, meter metric.Meter, r *Repor
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_up",
|
||||
"Whether UAP is up (1) or down (0)", up, attrs)
|
||||
|
||||
u.exportLTE(ctx, meter, r, s)
|
||||
|
||||
for _, radio := range s.RadioTable {
|
||||
radioAttrs := attribute.NewSet(
|
||||
attribute.String("mac", s.Mac),
|
||||
@@ -85,3 +87,49 @@ func (u *OtelOutput) exportUAP(ctx context.Context, meter metric.Meter, r *Repor
|
||||
"UAP VAP transmit bytes total", vap.TxBytes.Val, vapAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
// hasLTEStatus reports whether this access point is a cellular backup module.
|
||||
// U-LTE and U-LTE-Pro are typed as UAPs; their live modem status arrives as lte_* fields.
|
||||
func hasLTEStatus(d *unifi.UAP) bool {
|
||||
return d.LteState.Txt != "" || d.LteFailoverMode != "" || d.LteRat != "" ||
|
||||
d.LteNetworkOperator != "" || d.LteSignal != "" || d.LteConnected.Txt != ""
|
||||
}
|
||||
|
||||
// exportLTE emits live cellular backup status for a U-LTE or U-LTE-Pro module.
|
||||
func (u *OtelOutput) exportLTE(ctx context.Context, meter metric.Meter, r *Report, s *unifi.UAP) {
|
||||
if !hasLTEStatus(s) {
|
||||
return
|
||||
}
|
||||
|
||||
attrs := attribute.NewSet(
|
||||
attribute.String("mac", s.Mac),
|
||||
attribute.String("site_name", s.SiteName),
|
||||
attribute.String("source", s.SourceName),
|
||||
attribute.String("name", s.Name),
|
||||
attribute.String("model", s.Model),
|
||||
attribute.String("type", s.Type),
|
||||
attribute.String("lte_state", s.LteState.Txt),
|
||||
attribute.String("lte_failover_mode", s.LteFailoverMode),
|
||||
attribute.String("lte_signal", s.LteSignal),
|
||||
attribute.String("lte_rat", s.LteRat),
|
||||
attribute.String("lte_mode", s.LteMode),
|
||||
attribute.String("lte_band", s.LteBand),
|
||||
attribute.String("lte_pdp_type", s.LtePdpType),
|
||||
attribute.String("lte_operator", s.LteNetworkOperator),
|
||||
)
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_connected",
|
||||
"LTE modem connected to the carrier (1) or not (0)", s.LteConnected.Float64(), attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_failover",
|
||||
"LTE failover carrying traffic (1) or standing by (0)", s.LteFailover.Float64(), attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_rssi_dbm",
|
||||
"LTE RSSI in dBm", s.LteRssi.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_rsrp_dbm",
|
||||
"LTE RSRP in dBm", s.LteRsrp.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_rsrq_db",
|
||||
"LTE RSRQ in dB", s.LteRsrq.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_rx_channel",
|
||||
"LTE receive channel", s.LteRxChannel.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_uap_lte_tx_channel",
|
||||
"LTE transmit channel", s.LteTxChannel.Val, attrs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//nolint:testpackage // white-box: exercises the unexported descriptors and export path.
|
||||
package promunifi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// Captured from a U-LTE-Pro (model ULTEPEU) while the module was in
|
||||
// "LTE Failover Ready" / SIM standby. IMEI, ICCID, and cell identity are omitted.
|
||||
const ulteStandbyJSON = `{
|
||||
"adopted": true,
|
||||
"type": "uap",
|
||||
"model": "ULTEPEU",
|
||||
"name": "U-LTE-Pro",
|
||||
"mac": "aa:bb:cc:dd:ee:ff",
|
||||
"lte_state": "ready",
|
||||
"lte_failover": false,
|
||||
"lte_failover_mode": "failover",
|
||||
"lte_connected": "yes",
|
||||
"lte_networkoperator": "o2 - de",
|
||||
"lte_signal": "Good signal strength (3)",
|
||||
"lte_rssi": "-58",
|
||||
"lte_rsrp": "-95",
|
||||
"lte_rsrq": "-16",
|
||||
"lte_rat": "LTE",
|
||||
"lte_mode": "LTE",
|
||||
"lte_band": "eutran-3",
|
||||
"lte_rx_chan": "1600",
|
||||
"lte_tx_chan": "19600",
|
||||
"lte_pdptype": "IPV4"
|
||||
}`
|
||||
|
||||
func testProm() *promUnifi {
|
||||
return &promUnifi{
|
||||
Device: descDevice("unifi_"),
|
||||
UAP: descUAP("unifi_"),
|
||||
USG: descUSG("unifi_"),
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalULTE(t *testing.T, payload string) *unifi.UAP {
|
||||
t.Helper()
|
||||
|
||||
var d unifi.UAP
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &d))
|
||||
|
||||
d.SiteName = "Default"
|
||||
d.SourceName = "https://udm.example"
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
func TestExportULTEStandbyStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := unmarshalULTE(t, ulteStandbyJSON)
|
||||
r := &fakeReport{}
|
||||
testProm().exportUAP(r, d)
|
||||
|
||||
got := metricsByName(t, r.sent)
|
||||
a := assert.New(t)
|
||||
|
||||
a.Equal(1.0, got["unifi_lte_connected"].value)
|
||||
a.Equal(0.0, got["unifi_lte_failover"].value)
|
||||
a.InDelta(-58, got["unifi_lte_rssi_dbm"].value, 0.001)
|
||||
a.InDelta(-95, got["unifi_lte_rsrp_dbm"].value, 0.001)
|
||||
a.InDelta(-16, got["unifi_lte_rsrq_db"].value, 0.001)
|
||||
a.InDelta(1600, got["unifi_lte_rx_channel"].value, 0.001)
|
||||
a.InDelta(19600, got["unifi_lte_tx_channel"].value, 0.001)
|
||||
a.Equal(1.0, got["unifi_lte_info"].value)
|
||||
a.Equal([]string{
|
||||
"uap", "Default", "U-LTE-Pro", "https://udm.example", "",
|
||||
"ready", "failover", "Good signal strength (3)", "LTE", "LTE", "eutran-3", "IPV4", "o2 - de",
|
||||
}, got["unifi_lte_info"].labels)
|
||||
}
|
||||
|
||||
func TestExportULTEFailoverActive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := unmarshalULTE(t, ulteStandbyJSON)
|
||||
require.NoError(t, json.Unmarshal([]byte(`true`), &d.LteFailover))
|
||||
|
||||
r := &fakeReport{}
|
||||
testProm().exportUAP(r, d)
|
||||
|
||||
got := metricsByName(t, r.sent)
|
||||
assert.Equal(t, 1.0, got["unifi_lte_failover"].value)
|
||||
assert.Equal(t, 1.0, got["unifi_lte_connected"].value)
|
||||
}
|
||||
|
||||
func TestExportUAPWithoutLTEOmitsModemMetrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &unifi.UAP{
|
||||
Adopted: unifi.FlexBool{Val: true, Txt: "true"},
|
||||
Type: "uap",
|
||||
Name: "Hallway",
|
||||
SiteName: "Default",
|
||||
SourceName: "https://udm.example",
|
||||
}
|
||||
r := &fakeReport{}
|
||||
testProm().exportUAP(r, d)
|
||||
|
||||
for name := range metricsByName(t, r.sent) {
|
||||
assert.NotContains(t, name, "lte", "a wireless AP must not emit modem metrics")
|
||||
}
|
||||
}
|
||||
|
||||
type recordedMetric struct {
|
||||
value float64
|
||||
labels []string
|
||||
}
|
||||
|
||||
func metricsByName(t *testing.T, sent []*metric) map[string]recordedMetric {
|
||||
t.Helper()
|
||||
|
||||
out := make(map[string]recordedMetric, len(sent))
|
||||
|
||||
for _, m := range sent {
|
||||
name := prometheusDescName(t, m.Desc)
|
||||
out[name] = recordedMetric{value: metricFloat(t, m), labels: m.Labels}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func metricFloat(t *testing.T, m *metric) float64 {
|
||||
t.Helper()
|
||||
|
||||
switch v := m.Value.(type) {
|
||||
case float64:
|
||||
return v
|
||||
case unifi.FlexInt:
|
||||
return v.Val
|
||||
case bool:
|
||||
if v {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
default:
|
||||
t.Fatalf("metric value is %T", m.Value)
|
||||
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,15 @@ type uap struct {
|
||||
RadioNumSta *prometheus.Desc
|
||||
RadioTxPackets *prometheus.Desc
|
||||
RadioTxRetries *prometheus.Desc
|
||||
// LTE backup modem (U-LTE / U-LTE-Pro). These devices are typed as UAPs.
|
||||
LteConnected *prometheus.Desc
|
||||
LteFailover *prometheus.Desc
|
||||
LteRssi *prometheus.Desc
|
||||
LteRsrp *prometheus.Desc
|
||||
LteRsrq *prometheus.Desc
|
||||
LteRxChannel *prometheus.Desc
|
||||
LteTxChannel *prometheus.Desc
|
||||
LteInfo *prometheus.Desc
|
||||
}
|
||||
|
||||
type rogueap struct {
|
||||
@@ -114,6 +123,11 @@ func descUAP(ns string) *uap { // nolint: funlen
|
||||
labelA := []string{"stat", "site_name", "name", "source", "tag"} // stat + labels[1:]
|
||||
labelV := []string{"vap_name", "bssid", "radio", "band", "radio_name", "essid", "usage", "mac", "site_name", "name", "source", "tag"}
|
||||
labelR := []string{"radio_name", "radio", "band", "mac", "site_name", "name", "source", "tag"}
|
||||
labelL := []string{"type", "site_name", "name", "source", "tag"}
|
||||
labelLI := []string{
|
||||
"type", "site_name", "name", "source", "tag",
|
||||
"lte_state", "failover_mode", "signal", "rat", "mode", "band", "pdp_type", "operator",
|
||||
}
|
||||
nd := prometheus.NewDesc
|
||||
|
||||
return &uap{
|
||||
@@ -189,6 +203,14 @@ func descUAP(ns string) *uap { // nolint: funlen
|
||||
RadioNumSta: nd(ns+"radio_stations", "Radio Total Station Count", append(labelR, "station_type"), nil),
|
||||
RadioTxPackets: nd(ns+"radio_transmit_packets", "Radio Transmitted Packets", labelR, nil),
|
||||
RadioTxRetries: nd(ns+"radio_transmit_retries", "Radio Transmit Retries", labelR, nil),
|
||||
LteConnected: nd(ns+"lte_connected", "LTE modem connected to the carrier (1) or not (0). U-LTE devices only.", labelL, nil),
|
||||
LteFailover: nd(ns+"lte_failover", "LTE failover is carrying traffic (1) or standing by (0). U-LTE devices only.", labelL, nil),
|
||||
LteRssi: nd(ns+"lte_rssi_dbm", "LTE RSSI in dBm. U-LTE devices only.", labelL, nil),
|
||||
LteRsrp: nd(ns+"lte_rsrp_dbm", "LTE RSRP in dBm. U-LTE devices only.", labelL, nil),
|
||||
LteRsrq: nd(ns+"lte_rsrq_db", "LTE RSRQ in dB. U-LTE devices only.", labelL, nil),
|
||||
LteRxChannel: nd(ns+"lte_rx_channel", "LTE receive channel. U-LTE devices only.", labelL, nil),
|
||||
LteTxChannel: nd(ns+"lte_tx_channel", "LTE transmit channel. U-LTE devices only.", labelL, nil),
|
||||
LteInfo: nd(ns+"lte_info", "LTE modem state. Always 1; carrier and signal details are labels. U-LTE devices only.", labelLI, nil),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +252,7 @@ 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, d.Mac)
|
||||
u.exportLTE(r, d, labels)
|
||||
// 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.
|
||||
@@ -401,3 +424,39 @@ func (u *promUnifi) exportRADtable(r report, labels []string, rt unifi.RadioTabl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasLTEStatus reports whether this access point is a cellular backup module.
|
||||
// U-LTE and U-LTE-Pro are typed as UAPs; their live modem status arrives as lte_* fields
|
||||
// on the device list, not on the gateway WAN ports.
|
||||
func hasLTEStatus(d *unifi.UAP) bool {
|
||||
return d.LteState.Txt != "" || d.LteFailoverMode != "" || d.LteRat != "" ||
|
||||
d.LteNetworkOperator != "" || d.LteSignal != "" || d.LteConnected.Txt != ""
|
||||
}
|
||||
|
||||
func (u *promUnifi) exportLTE(r report, d *unifi.UAP, labels []string) {
|
||||
if !hasLTEStatus(d) {
|
||||
return
|
||||
}
|
||||
|
||||
info := append(append([]string{}, labels...),
|
||||
d.LteState.Txt,
|
||||
d.LteFailoverMode,
|
||||
d.LteSignal,
|
||||
d.LteRat,
|
||||
d.LteMode,
|
||||
d.LteBand,
|
||||
d.LtePdpType,
|
||||
d.LteNetworkOperator,
|
||||
)
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UAP.LteConnected, gauge, d.LteConnected.Val, labels},
|
||||
{u.UAP.LteFailover, gauge, d.LteFailover.Val, labels},
|
||||
{u.UAP.LteRssi, gauge, d.LteRssi, labels},
|
||||
{u.UAP.LteRsrp, gauge, d.LteRsrp, labels},
|
||||
{u.UAP.LteRsrq, gauge, d.LteRsrq, labels},
|
||||
{u.UAP.LteRxChannel, gauge, d.LteRxChannel, labels},
|
||||
{u.UAP.LteTxChannel, gauge, d.LteTxChannel, labels},
|
||||
{u.UAP.LteInfo, gauge, 1.0, info},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user