feat(umbb): review fixes for cellular modem metrics

- Skip signal readings and max bitrates the modem does not report,
  so a missing reading is not exported as 0 dBm.
- Export the 5G NR leg (rsrp_nr, rsrq_nr, snr_nr) separately; the
  plain values are the LTE anchor in 5G NSA mode.
- Export max negotiated bitrate, internet connectivity, and per-SIM
  card present, PIN blocked, data warning and data limited.
- Add primary slot, MCC/MNC, cell ID, PCI and channel to mbb_info, and
  has_carrier/metered to mbb_sim_info.
- Rename the mbb_mode label/field to failover_mode, matching lte_info.
- Drop network_reject_age: its unit is undocumented and it does not
  behave like a running age.
- Clarify descriptions for registration_state, signal and SIM byte
  counters, and count UMBB devices in the Prometheus export log line.

Signed-off-by: Tom Wilkie <tom.wilkie@gmail.com>
This commit is contained in:
Tom Wilkie
2026-10-01 21:30:49 +01:00
parent 5ed9b00598
commit dd8a42476a
8 changed files with 307 additions and 84 deletions
@@ -567,19 +567,25 @@ gauges:
- unifi.udb.user_num_sta
- unifi.udb.user_wlan_num_sta
- unifi.umbb_sim.active
- unifi.umbb_sim.card_present
- unifi.umbb_sim.data_limited
- unifi.umbb_sim.data_warning
- unifi.umbb_sim.display_state_elapsed
- unifi.umbb_sim.network_reject_age
- unifi.umbb_sim.pin_blocked
- unifi.umbb_sim.rx_bytes
- unifi.umbb_sim.tx_bytes
- unifi.umbb.cpu
- unifi.umbb.current_slot
- unifi.umbb.dl_bandwidth_mhz
- unifi.umbb.has_coverage
- unifi.umbb.internet
- unifi.umbb.last_seen
- unifi.umbb.loadavg_1
- unifi.umbb.loadavg_15
- unifi.umbb.loadavg_5
- unifi.umbb.lte_carriers
- unifi.umbb.lte_max_bitrate_dl
- unifi.umbb.lte_max_bitrate_ul
- unifi.umbb.mem
- unifi.umbb.mem_buffer
- unifi.umbb.mem_total
@@ -587,6 +593,12 @@ gauges:
- unifi.umbb.memory
- unifi.umbb.network
- unifi.umbb.nr_carriers
- unifi.umbb.nr_max_bitrate_dl
- unifi.umbb.nr_max_bitrate_ul
- unifi.umbb.nr_rsrp
- unifi.umbb.nr_rsrq
- unifi.umbb.nr_snr
- unifi.umbb.primary_slot
- unifi.umbb.probe
- unifi.umbb.registration_state
- unifi.umbb.roaming
+51 -21
View File
@@ -21,20 +21,20 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
dl, ul := mbbBandwidth(rad)
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,
"mbb_state": s.Mbb.State,
"mbb_mode": s.Mbb.Mode,
"rat": rad.Rat,
"band": rad.Band,
"operator": rad.NetworkOperator,
"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,
"mbb_state": s.Mbb.State,
"failover_mode": s.Mbb.Mode,
"rat": rad.Rat,
"band": rad.Band,
"operator": rad.NetworkOperator,
})
data := CombineFloat64(
@@ -50,11 +50,8 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
"uplink_uptime": s.Uplink.Uptime.Val,
"sa_mode": rad.SA5GMode.Float64(),
"current_slot": rad.CurrentSlot.Val,
"rsrp": rad.Rsrp.Val,
"rsrq": rad.Rsrq.Val,
"snr": rad.Snr.Val,
"signal": rad.Signal.Val,
"signal_percent": rad.SignalPercent.Val,
"primary_slot": s.MbbOverrides.PrimarySlot.Val,
"internet": s.Internet.Float64(),
"has_coverage": rad.HasCoverage.Float64(),
"roaming": rad.Roaming.Float64(),
"registration_state": rad.RegistrationState.Val,
@@ -62,7 +59,8 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
"lte_carriers": float64(len(rad.CaLte)),
"dl_bandwidth_mhz": dl,
"ul_bandwidth_mhz": ul,
})
},
mbbOptional(rad))
r.addCount(umbbT)
reportGaugeForFloat64Map(r, metricNamespace("umbb"), data, tags)
@@ -79,13 +77,18 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
"esim": strconv.FormatBool(sim.Esim.Val),
"spn": sim.Spn,
"display_state": sim.DisplayState,
"has_carrier": strconv.FormatBool(sim.HasCarrier.Val),
"metered": strconv.FormatBool(sim.Metered.Val),
"network_reject_type": sim.NetworkRejectType,
"network_reject_text": sim.NetworkRejectText,
})
simData := map[string]float64{
"active": sim.Active.Float64(),
"card_present": sim.CardPresent.Float64(),
"pin_blocked": sim.PinBlocked.Float64(),
"data_warning": sim.DataWarning.Float64(),
"data_limited": sim.DataLimited.Float64(),
"display_state_elapsed": sim.DisplayStateElapsed.Val,
"network_reject_age": sim.NetworkRejectAge.Val,
"rx_bytes": sim.RxBytes.Val,
"tx_bytes": sim.TxBytes.Val,
}
@@ -94,6 +97,33 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
}
}
// mbbOptional returns the signal readings and bitrates the modem reported, keyed by field name.
// Missing readings are left out, so they are not mistaken for 0 dBm.
func mbbOptional(rad unifi.MBBRadio) map[string]float64 {
out := map[string]float64{}
for k, v := range map[string]unifi.FlexInt{
"rsrp": rad.Rsrp,
"rsrq": rad.Rsrq,
"snr": rad.Snr,
"nr_rsrp": rad.RsrpNr,
"nr_rsrq": rad.RsrqNr,
"nr_snr": rad.SnrNr,
"signal": rad.Signal,
"signal_percent": rad.SignalPercent,
"lte_max_bitrate_dl": rad.MaxBitrateDl,
"lte_max_bitrate_ul": rad.MaxBitrateUl,
"nr_max_bitrate_dl": rad.MaxBitrateDlNr,
"nr_max_bitrate_ul": rad.MaxBitrateUlNr,
} {
if v.Txt != "" {
out[k] = v.Val
}
}
return out
}
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
@@ -683,24 +683,38 @@ points:
- version
fields:
band: string
cell_id: float
channel: float
cpu: float
current_slot: float
dl_bandwidth_mhz: float
failover_mode: string
has_coverage: bool
internet: bool
ip: string
last_seen: float
loadavg_1: float
loadavg_5: float
loadavg_15: float
lte_carriers: float
mbb_mode: string
lte_max_bitrate_dl: float
lte_max_bitrate_ul: float
mbb_state: string
mcc: float
mem: float
mem_buffer: float
mem_total: float
mem_used: float
mnc: float
nr_carriers: float
nr_max_bitrate_dl: float
nr_max_bitrate_ul: float
nr_rsrp: float
nr_rsrq: float
nr_snr: float
operator: string
pci: float
primary_slot: float
rat: string
registration_state: float
roaming: bool
@@ -737,11 +751,16 @@ points:
- type
fields:
active: bool
card_present: bool
data_limited: bool
data_warning: bool
display_state: string
display_state_elapsed: float
network_reject_age: float
has_carrier: bool
metered: bool
network_reject_text: string
network_reject_type: string
pin_blocked: bool
rx_bytes: float
tx_bytes: float
unifi_alarm:
+45 -7
View File
@@ -43,18 +43,20 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) {
"uplink_max_speed": s.Uplink.MaxSpeed.Val,
"uplink_latency": s.Uplink.Latency.Val,
"uplink_uptime": s.Uplink.Uptime.Val,
"internet": s.Internet.Val,
"mbb_state": s.Mbb.State,
"mbb_mode": s.Mbb.Mode,
"failover_mode": s.Mbb.Mode,
"rat": rad.Rat,
"band": rad.Band,
"operator": rad.NetworkOperator,
"sa_mode": rad.SA5GMode.Val,
"current_slot": rad.CurrentSlot.Val,
"rsrp": rad.Rsrp.Val,
"rsrq": rad.Rsrq.Val,
"snr": rad.Snr.Val,
"signal": rad.Signal.Val,
"signal_percent": rad.SignalPercent.Val,
"primary_slot": s.MbbOverrides.PrimarySlot.Val,
"mcc": rad.Mcc.Val,
"mnc": rad.Mnc.Val,
"cell_id": rad.CellID.Val,
"pci": rad.Pci.Val,
"channel": rad.Channel.Val,
"has_coverage": rad.HasCoverage.Val,
"roaming": rad.Roaming.Val,
"registration_state": rad.RegistrationState.Val,
@@ -64,6 +66,10 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) {
"ul_bandwidth_mhz": ul,
})
for k, v := range mbbOptional(rad) {
fields[k] = v
}
r.addCount(umbbT)
r.send(&metric{Table: "umbb", Tags: tags, Fields: fields})
@@ -83,11 +89,16 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) {
},
Fields: map[string]any{
"active": sim.Active.Val,
"card_present": sim.CardPresent.Val,
"pin_blocked": sim.PinBlocked.Val,
"has_carrier": sim.HasCarrier.Val,
"metered": sim.Metered.Val,
"data_warning": sim.DataWarning.Val,
"data_limited": sim.DataLimited.Val,
"display_state": sim.DisplayState,
"display_state_elapsed": sim.DisplayStateElapsed.Val,
"network_reject_type": sim.NetworkRejectType,
"network_reject_text": sim.NetworkRejectText,
"network_reject_age": sim.NetworkRejectAge.Val,
"rx_bytes": sim.RxBytes.Val,
"tx_bytes": sim.TxBytes.Val,
},
@@ -95,6 +106,33 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) {
}
}
// mbbOptional returns the signal readings and bitrates the modem reported, keyed by field name.
// Missing readings are left out, so they are not mistaken for 0 dBm.
func mbbOptional(rad unifi.MBBRadio) map[string]float64 {
out := map[string]float64{}
for k, v := range map[string]unifi.FlexInt{
"rsrp": rad.Rsrp,
"rsrq": rad.Rsrq,
"snr": rad.Snr,
"nr_rsrp": rad.RsrpNr,
"nr_rsrq": rad.RsrqNr,
"nr_snr": rad.SnrNr,
"signal": rad.Signal,
"signal_percent": rad.SignalPercent,
"lte_max_bitrate_dl": rad.MaxBitrateDl,
"lte_max_bitrate_ul": rad.MaxBitrateUl,
"nr_max_bitrate_dl": rad.MaxBitrateDlNr,
"nr_max_bitrate_ul": rad.MaxBitrateUlNr,
} {
if v.Txt != "" {
out[k] = v.Val
}
}
return out
}
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
+42 -16
View File
@@ -28,7 +28,7 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo
attribute.String("type", s.Type),
attribute.String("ip", s.IP),
attribute.String("mbb_state", s.Mbb.State),
attribute.String("mbb_mode", s.Mbb.Mode),
attribute.String("failover_mode", s.Mbb.Mode),
attribute.String("rat", rad.Rat),
attribute.String("band", rad.Band),
attribute.String("operator", rad.NetworkOperator),
@@ -47,22 +47,40 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo
"UMBB CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_mem_utilization",
"UMBB memory utilization percentage", s.SystemStats.Mem.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_rsrp_dbm",
"Cellular RSRP in dBm", rad.Rsrp.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_rsrq_db",
"Cellular RSRQ in dB", rad.Rsrq.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_snr_db",
"Cellular signal-to-noise ratio in dB", rad.Snr.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_signal_bars",
"Cellular signal strength in bars (0-5)", rad.Signal.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_signal_percent",
"Cellular signal strength percentage", rad.SignalPercent.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_internet",
"Modem reports internet connectivity (1) or not (0)", s.Internet.Float64(), attrs)
// Signal readings and bitrates are left out when the modem does not report them,
// so a missing reading is not mistaken for 0 dBm.
for _, g := range []struct {
name, desc string
val unifi.FlexInt
}{
{"unifi_device_umbb_rsrp_dbm", "Cellular RSRP of the serving cell in dBm. LTE anchor in 5G NSA mode", rad.Rsrp},
{"unifi_device_umbb_rsrq_db", "Cellular RSRQ of the serving cell in dB. LTE anchor in 5G NSA mode", rad.Rsrq},
{"unifi_device_umbb_snr_db", "Cellular signal-to-noise ratio of the serving cell in dB. LTE anchor in 5G NSA mode", rad.Snr},
{"unifi_device_umbb_nr_rsrp_dbm", "5G NR RSRP in dBm", rad.RsrpNr},
{"unifi_device_umbb_nr_rsrq_db", "5G NR RSRQ in dB", rad.RsrqNr},
{"unifi_device_umbb_nr_snr_db", "5G NR signal-to-noise ratio in dB", rad.SnrNr},
{"unifi_device_umbb_signal_bars", "Cellular signal strength in bars, as shown by the controller", rad.Signal},
{"unifi_device_umbb_signal_percent", "Cellular signal strength percentage (0-100)", rad.SignalPercent},
{"unifi_device_umbb_lte_max_bitrate_dl_bps", "Maximum LTE downlink bitrate in bits per second", rad.MaxBitrateDl},
{"unifi_device_umbb_lte_max_bitrate_ul_bps", "Maximum LTE uplink bitrate in bits per second", rad.MaxBitrateUl},
{"unifi_device_umbb_nr_max_bitrate_dl_bps", "Maximum 5G NR downlink bitrate in bits per second", rad.MaxBitrateDlNr},
{"unifi_device_umbb_nr_max_bitrate_ul_bps", "Maximum 5G NR uplink bitrate in bits per second", rad.MaxBitrateUlNr},
} {
if g.val.Txt != "" {
u.recordGauge(ctx, meter, r, g.name, g.desc, g.val.Val, attrs)
}
}
u.recordGauge(ctx, meter, r, "unifi_device_umbb_coverage",
"Modem has cellular coverage (1) or not (0)", rad.HasCoverage.Float64(), attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_roaming",
"Modem is roaming (1) or on its home network (0)", rad.Roaming.Float64(), attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_registration_state",
"Cellular network registration state code", rad.RegistrationState.Val, attrs)
"Cellular network registration state code (undocumented; likely 0 not registered, 1 home, "+
"2 searching, 3 denied, 4 unknown, 5 roaming)", rad.RegistrationState.Val, attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_nr_carriers",
"Aggregated 5G NR component carriers in use", float64(len(rad.CaNr)), attrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_lte_carriers",
@@ -85,6 +103,8 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo
attribute.String("esim", strconv.FormatBool(sim.Esim.Val)),
attribute.String("spn", sim.Spn),
attribute.String("display_state", sim.DisplayState),
attribute.Bool("has_carrier", sim.HasCarrier.Val),
attribute.Bool("metered", sim.Metered.Val),
attribute.String("network_reject_type", sim.NetworkRejectType),
attribute.String("network_reject_text", sim.NetworkRejectText),
)
@@ -93,12 +113,18 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo
"SIM slot is the active data SIM (1) or not (0)", sim.Active.Float64(), simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_state_seconds",
"Seconds the SIM has been in its current display state", sim.DisplayStateElapsed.Val, simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_network_reject_age",
"Age of the last carrier network reject, as reported by the controller", sim.NetworkRejectAge.Val, simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_card_present",
"SIM card or eSIM profile is present (1) or not (0)", sim.CardPresent.Float64(), simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_pin_blocked",
"SIM is PIN blocked (1) or not (0)", sim.PinBlocked.Float64(), simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_data_warning",
"SIM has passed its data usage warning threshold (1) or not (0)", sim.DataWarning.Float64(), simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_data_limited",
"SIM has reached its data limit (1) or not (0)", sim.DataLimited.Float64(), simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_receive_bytes_total",
"Bytes received over this SIM", sim.RxBytes.Val, simAttrs)
"Bytes received over this SIM. Cumulative; not reset when the modem reboots", sim.RxBytes.Val, simAttrs)
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_transmit_bytes_total",
"Bytes transmitted over this SIM", sim.TxBytes.Val, simAttrs)
"Bytes transmitted over this SIM. Cumulative; not reset when the modem reboots", sim.TxBytes.Val, simAttrs)
}
}
+2 -2
View File
@@ -52,9 +52,9 @@ func (r *Report) report(c poller.Logger, descs map[*prometheus.Desc]bool) {
m := r.Metrics
c.Logf("UniFi Measurements Exported. Site: %d, Client: %d, "+
"UAP: %d, USG/UDM: %d, USW: %d, DPI Site/Client: %d/%d, Countries: %d, Desc: %d, "+
"UAP: %d, USG/UDM: %d, USW: %d, UMBB: %d, DPI Site/Client: %d/%d, Countries: %d, Desc: %d, "+
"Metric: %d, Bytes: %d, Err: %d, 0s: %d, Req/Total: %v / %v",
len(m.Sites), len(m.Clients), r.UAP, r.UDM+r.USG+r.UXG, r.USW, len(m.SitesDPI),
len(m.Sites), len(m.Clients), r.UAP, r.UDM+r.USG+r.UXG, r.USW, r.UMBB, len(m.SitesDPI),
len(m.ClientsDPI), len(m.CountryTraffic), len(descs), r.Total, r.Bytes, r.Errors, r.Zeros,
r.Fetch.Round(time.Millisecond/oneDecimalPoint),
r.Elapsed.Round(time.Millisecond/oneDecimalPoint))
+82 -29
View File
@@ -10,9 +10,13 @@ import (
// umbb holds the cellular modem descriptors for UMBB (mobile broadband) devices, such as the U5G Max.
type umbb struct {
Internet *prometheus.Desc
Rsrp *prometheus.Desc
Rsrq *prometheus.Desc
Snr *prometheus.Desc
RsrpNr *prometheus.Desc
RsrqNr *prometheus.Desc
SnrNr *prometheus.Desc
SignalBars *prometheus.Desc
SignalPercent *prometheus.Desc
Coverage *prometheus.Desc
@@ -20,12 +24,16 @@ type umbb struct {
RegistrationState *prometheus.Desc
Carriers *prometheus.Desc
BandwidthMhz *prometheus.Desc
MaxBitrate *prometheus.Desc
Info *prometheus.Desc
SimActive *prometheus.Desc
SimCardPresent *prometheus.Desc
SimPinBlocked *prometheus.Desc
SimDataWarning *prometheus.Desc
SimDataLimited *prometheus.Desc
SimRxBytes *prometheus.Desc
SimTxBytes *prometheus.Desc
SimStateSeconds *prometheus.Desc
SimRejectAge *prometheus.Desc
SimInfo *prometheus.Desc
}
@@ -35,26 +43,44 @@ func descUMBB(ns string) *umbb {
nd := prometheus.NewDesc
return &umbb{
Rsrp: nd(ns+"mbb_rsrp_dbm", "Cellular RSRP in dBm.", labels, nil),
Rsrq: nd(ns+"mbb_rsrq_db", "Cellular RSRQ in dB.", labels, nil),
Snr: nd(ns+"mbb_snr_db", "Cellular signal-to-noise ratio in dB.", labels, nil),
SignalBars: nd(ns+"mbb_signal_bars", "Cellular signal strength in bars (0-5).", labels, nil),
SignalPercent: nd(ns+"mbb_signal_percent", "Cellular signal strength percentage.", labels, nil),
Coverage: nd(ns+"mbb_coverage", "Modem has cellular coverage (1) or not (0).", labels, nil),
Roaming: nd(ns+"mbb_roaming", "Modem is roaming (1) or on its home network (0).", labels, nil),
RegistrationState: nd(ns+"mbb_registration_state", "Cellular network registration state code.", labels, nil),
Carriers: nd(ns+"mbb_carriers", "Aggregated component carriers in use.", append(labels, "carrier_type"), nil),
BandwidthMhz: nd(ns+"mbb_bandwidth_mhz", "Total aggregated carrier bandwidth in MHz.", append(labels, "direction"), nil),
Internet: nd(ns+"mbb_internet", "Modem reports internet connectivity (1) or not (0).", labels, nil),
Rsrp: nd(ns+"mbb_rsrp_dbm", "Cellular RSRP of the serving cell in dBm. LTE anchor in 5G NSA mode.", labels, nil),
Rsrq: nd(ns+"mbb_rsrq_db", "Cellular RSRQ of the serving cell in dB. LTE anchor in 5G NSA mode.", labels, nil),
Snr: nd(ns+"mbb_snr_db", "Cellular signal-to-noise ratio of the serving cell in dB. LTE anchor in 5G NSA mode.", labels, nil),
RsrpNr: nd(ns+"mbb_nr_rsrp_dbm", "5G NR RSRP in dBm. Same as mbb_rsrp_dbm in 5G SA mode.", labels, nil),
RsrqNr: nd(ns+"mbb_nr_rsrq_db", "5G NR RSRQ in dB. Same as mbb_rsrq_db in 5G SA mode.", labels, nil),
SnrNr: nd(ns+"mbb_nr_snr_db", "5G NR signal-to-noise ratio in dB. Same as mbb_snr_db in 5G SA mode.", labels, nil),
SignalBars: nd(ns+"mbb_signal_bars",
"Cellular signal strength in bars, as shown by the controller.", labels, nil),
SignalPercent: nd(ns+"mbb_signal_percent",
"Cellular signal strength percentage (0-100), as computed by the controller.", labels, nil),
Coverage: nd(ns+"mbb_coverage", "Modem has cellular coverage (1) or not (0).", labels, nil),
Roaming: nd(ns+"mbb_roaming", "Modem is roaming (1) or on its home network (0).", labels, nil),
RegistrationState: nd(ns+"mbb_registration_state",
"Cellular network registration state code. Undocumented; likely 0 not registered, 1 registered home, "+
"2 searching, 3 denied, 4 unknown, 5 registered roaming.", labels, nil),
Carriers: nd(ns+"mbb_carriers", "Aggregated component carriers in use.", append(labels, "carrier_type"), nil),
BandwidthMhz: nd(ns+"mbb_bandwidth_mhz", "Total aggregated carrier bandwidth in MHz.",
append(labels, "direction"), nil),
MaxBitrate: nd(ns+"mbb_max_bitrate_bps", "Maximum bitrate negotiated with the network, in bits per second.",
append(labels, "carrier_type", "direction"), nil),
Info: nd(ns+"mbb_info", "Cellular modem state. Always 1; state and network details are labels.",
append(labels, "mbb_state", "mbb_mode", "rat", "band", "operator", "sa_mode", "current_slot"), nil),
SimActive: nd(ns+"mbb_sim_active", "SIM slot is the active data SIM (1) or not (0).", labelS, nil),
SimRxBytes: nd(ns+"mbb_sim_receive_bytes_total", "Bytes received over this SIM.", labelS, nil),
SimTxBytes: nd(ns+"mbb_sim_transmit_bytes_total", "Bytes transmitted over this SIM.", labelS, nil),
SimStateSeconds: nd(ns+"mbb_sim_state_seconds", "Seconds the SIM has been in its current display state.", labelS, nil),
SimRejectAge: nd(ns+"mbb_sim_network_reject_age",
"Age of the last carrier network reject, as reported by the controller.", labelS, nil),
append(labels, "mbb_state", "failover_mode", "rat", "band", "operator", "sa_mode", "current_slot",
"primary_slot", "mcc", "mnc", "cell_id", "pci", "channel"), nil),
SimActive: nd(ns+"mbb_sim_active", "SIM slot is the active data SIM (1) or not (0).", labelS, nil),
SimCardPresent: nd(ns+"mbb_sim_card_present", "SIM card or eSIM profile is present (1) or not (0).", labelS, nil),
SimPinBlocked: nd(ns+"mbb_sim_pin_blocked", "SIM is PIN blocked (1) or not (0).", labelS, nil),
SimDataWarning: nd(ns+"mbb_sim_data_warning", "SIM has passed its data usage warning threshold (1) or not (0).",
labelS, nil),
SimDataLimited: nd(ns+"mbb_sim_data_limited", "SIM has reached its data limit (1) or not (0).", labelS, nil),
SimRxBytes: nd(ns+"mbb_sim_receive_bytes_total",
"Bytes received over this SIM. Cumulative; not reset when the modem reboots.", labelS, nil),
SimTxBytes: nd(ns+"mbb_sim_transmit_bytes_total",
"Bytes transmitted over this SIM. Cumulative; not reset when the modem reboots.", labelS, nil),
SimStateSeconds: nd(ns+"mbb_sim_state_seconds", "Seconds the SIM has been in its current display state.",
labelS, nil),
SimInfo: nd(ns+"mbb_sim_info", "SIM state. Always 1; display state and last network reject are labels.",
append(labelS, "display_state", "network_reject_type", "network_reject_text"), nil),
append(labelS, "display_state", "has_carrier", "metered", "network_reject_type", "network_reject_text"), nil),
}
}
@@ -74,7 +100,7 @@ func (u *promUnifi) exportUMBB(r report, d *unifi.UMBB) {
u.exportSYSstats(r, labels, d.SysStats, d.SystemStats)
u.exportDeviceUplink(r, labels, d.Uplink)
u.exportMBB(r, labels, d.Mbb)
u.exportMBB(r, labels, d)
r.send([]*metric{
{u.Device.Info, gauge, 1.0, append(append([]string{}, baseLabels...), infoLabels...)},
@@ -84,19 +110,17 @@ func (u *promUnifi) exportUMBB(r report, d *unifi.UMBB) {
})
}
func (u *promUnifi) exportMBB(r report, labels []string, m unifi.MBB) {
func (u *promUnifi) exportMBB(r report, labels []string, d *unifi.UMBB) {
m := d.Mbb
rad := m.Radio
dl, ul := mbbBandwidth(rad)
info := append(append([]string{}, labels...),
m.State, m.Mode, rad.Rat, rad.Band, rad.NetworkOperator,
strconv.FormatBool(rad.SA5GMode.Val), rad.CurrentSlot.Txt)
strconv.FormatBool(rad.SA5GMode.Val), rad.CurrentSlot.Txt, d.MbbOverrides.PrimarySlot.Txt,
rad.Mcc.Txt, rad.Mnc.Txt, rad.CellID.Txt, rad.Pci.Txt, rad.Channel.Txt)
r.send([]*metric{
{u.UMBB.Rsrp, gauge, rad.Rsrp, labels},
{u.UMBB.Rsrq, gauge, rad.Rsrq, labels},
{u.UMBB.Snr, gauge, rad.Snr, labels},
{u.UMBB.SignalBars, gauge, rad.Signal, labels},
{u.UMBB.SignalPercent, gauge, rad.SignalPercent, labels},
{u.UMBB.Internet, gauge, d.Internet.Val, labels},
{u.UMBB.Coverage, gauge, rad.HasCoverage.Val, labels},
{u.UMBB.Roaming, gauge, rad.Roaming.Val, labels},
{u.UMBB.RegistrationState, gauge, rad.RegistrationState, labels},
@@ -107,18 +131,47 @@ func (u *promUnifi) exportMBB(r report, labels []string, m unifi.MBB) {
{u.UMBB.Info, gauge, 1.0, info},
})
// Signal readings and bitrates are left out when the modem does not report them,
// so a missing reading is not mistaken for 0 dBm.
for _, s := range []struct {
desc *prometheus.Desc
val unifi.FlexInt
dims []string
}{
{u.UMBB.Rsrp, rad.Rsrp, nil},
{u.UMBB.Rsrq, rad.Rsrq, nil},
{u.UMBB.Snr, rad.Snr, nil},
{u.UMBB.RsrpNr, rad.RsrpNr, nil},
{u.UMBB.RsrqNr, rad.RsrqNr, nil},
{u.UMBB.SnrNr, rad.SnrNr, nil},
{u.UMBB.SignalBars, rad.Signal, nil},
{u.UMBB.SignalPercent, rad.SignalPercent, nil},
{u.UMBB.MaxBitrate, rad.MaxBitrateDl, []string{"lte", "dl"}},
{u.UMBB.MaxBitrate, rad.MaxBitrateUl, []string{"lte", "ul"}},
{u.UMBB.MaxBitrate, rad.MaxBitrateDlNr, []string{"nr", "dl"}},
{u.UMBB.MaxBitrate, rad.MaxBitrateUlNr, []string{"nr", "ul"}},
} {
if s.val.Txt != "" {
r.send([]*metric{{s.desc, gauge, s.val.Val, append(append([]string{}, labels...), s.dims...)}})
}
}
for _, sim := range m.Sim {
simLabels := append(append([]string{}, labels...),
sim.Slot.Txt, strconv.FormatBool(sim.Esim.Val), sim.Spn)
simInfo := append(append([]string{}, simLabels...),
sim.DisplayState, sim.NetworkRejectType, sim.NetworkRejectText)
sim.DisplayState, strconv.FormatBool(sim.HasCarrier.Val), strconv.FormatBool(sim.Metered.Val),
sim.NetworkRejectType, sim.NetworkRejectText)
r.send([]*metric{
{u.UMBB.SimActive, gauge, sim.Active.Val, simLabels},
{u.UMBB.SimCardPresent, gauge, sim.CardPresent.Val, simLabels},
{u.UMBB.SimPinBlocked, gauge, sim.PinBlocked.Val, simLabels},
{u.UMBB.SimDataWarning, gauge, sim.DataWarning.Val, simLabels},
{u.UMBB.SimDataLimited, gauge, sim.DataLimited.Val, simLabels},
{u.UMBB.SimRxBytes, counter, sim.RxBytes, simLabels},
{u.UMBB.SimTxBytes, counter, sim.TxBytes, simLabels},
{u.UMBB.SimStateSeconds, gauge, sim.DisplayStateElapsed, simLabels},
{u.UMBB.SimRejectAge, gauge, sim.NetworkRejectAge, simLabels},
{u.UMBB.SimInfo, gauge, 1.0, simInfo},
})
}
+51 -6
View File
@@ -20,13 +20,16 @@ const u5gMaxJSON = `{
"mac": "00:00:5e:00:53:02",
"version": "7.5.3.19533",
"uptime": 5264387,
"internet": true,
"mbb_overrides": {"primary_slot": 1},
"mbb": {
"mode": "failover",
"state": "ready",
"sim": [
{"active": false, "slot": 1, "esim": false, "display_state": "no-sim", "display_state_elapsed": 5264323},
{"active": false, "slot": 1, "esim": false, "card_present": false, "display_state": "no-sim", "display_state_elapsed": 5264323},
{
"active": true, "slot": 2, "esim": true, "spn": "EE",
"active": true, "slot": 2, "esim": true, "spn": "EE", "card_present": true, "has_carrier": true,
"metered": false, "pin_blocked": false, "data_warning": true, "data_limited": false,
"display_state": "operational", "display_state_elapsed": 5230,
"network_reject_type": "wds", "network_reject_text": "Carrier rejected data", "network_reject_age": 45,
"rxbytes": "3013150278176", "txbytes": "725650860811"
@@ -35,6 +38,9 @@ const u5gMaxJSON = `{
"radio": {
"5g_sa_mode": true, "band": "n78", "rat": "5G", "networkoperator": "EE", "current_slot": 2,
"has_coverage": true, "roaming": false, "registration_state": 1,
"mcc": 234, "mnc": 30, "cell_id": 2719245, "pci": 216, "channel": 646080,
"max_bitrate_dl": 0, "max_bitrate_ul": 0, "max_bitrate_dl_nr": 640000000, "max_bitrate_ul_nr": 224000000,
"rsrp_nr": -96, "rsrq_nr": -13, "snr_nr": 17.600000381469727,
"rsrp": -96, "rsrq": -13, "snr": 17.600000381469727, "signal": 3, "signal_percent": 95,
"ca_lte": [],
"ca_nr": [
@@ -74,21 +80,60 @@ func TestExportUMBB(t *testing.T) {
a.InDelta(80, findMetric(t, r.sent, "unifi_mbb_bandwidth_mhz", append(labels, "dl")).value, 0.001)
a.InDelta(40, findMetric(t, r.sent, "unifi_mbb_bandwidth_mhz", append(labels, "ul")).value, 0.001,
"a null uplink bandwidth adds nothing")
a.InDelta(-96, findMetric(t, r.sent, "unifi_mbb_nr_rsrp_dbm", labels).value, 0.001)
a.InDelta(-13, findMetric(t, r.sent, "unifi_mbb_nr_rsrq_db", labels).value, 0.001)
a.InDelta(17.6, findMetric(t, r.sent, "unifi_mbb_nr_snr_db", labels).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_internet", labels).value, 0.001)
a.InDelta(640000000, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "nr", "dl")).value, 1)
a.InDelta(224000000, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "nr", "ul")).value, 1)
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "lte", "dl")).value, 1)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_info",
append(labels, "ready", "failover", "5G", "n78", "EE", "true", "2")).value, 0.001)
append(labels, "ready", "failover", "5G", "n78", "EE", "true", "2", "1",
"234", "30", "2719245", "216", "646080")).value, 0.001)
noSim := append(labels, "1", "false", "")
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_active", noSim).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_info", append(noSim, "no-sim", "", "")).value, 0.001)
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_card_present", noSim).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_info",
append(noSim, "no-sim", "false", "false", "", "")).value, 0.001)
esim := append(labels, "2", "true", "EE")
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_active", esim).value, 0.001)
a.InDelta(3013150278176, findMetric(t, r.sent, "unifi_mbb_sim_receive_bytes_total", esim).value, 1)
a.InDelta(725650860811, findMetric(t, r.sent, "unifi_mbb_sim_transmit_bytes_total", esim).value, 1)
a.InDelta(5230, findMetric(t, r.sent, "unifi_mbb_sim_state_seconds", esim).value, 0.001)
a.InDelta(45, findMetric(t, r.sent, "unifi_mbb_sim_network_reject_age", esim).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_card_present", esim).value, 0.001)
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_pin_blocked", esim).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_data_warning", esim).value, 0.001)
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_data_limited", esim).value, 0.001)
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_info",
append(esim, "operational", "wds", "Carrier rejected data")).value, 0.001)
append(esim, "operational", "true", "false", "wds", "Carrier rejected data")).value, 0.001)
}
func TestExportUMBBMissingSignalIsSkipped(t *testing.T) {
t.Parallel()
var d unifi.UMBB
require.NoError(t, json.Unmarshal([]byte(`{"adopted": true, "type": "umbb", "name": "U5G Max",
"mbb": {"state": "searching", "radio": {"has_coverage": false}}}`), &d))
r := &fakeReport{}
u := testProm()
u.UMBB = descUMBB("unifi_")
u.exportUMBB(r, &d)
for _, m := range r.sent {
name := prometheusDescName(t, m.Desc)
assert.NotContains(t, []string{
"unifi_mbb_rsrp_dbm", "unifi_mbb_rsrq_db", "unifi_mbb_snr_db",
"unifi_mbb_nr_rsrp_dbm", "unifi_mbb_nr_rsrq_db", "unifi_mbb_nr_snr_db",
"unifi_mbb_signal_bars", "unifi_mbb_signal_percent", "unifi_mbb_max_bitrate_bps",
}, name, "a reading the modem did not report must not be exported as 0")
}
labels := []string{"umbb", "", "U5G Max", "", ""}
assert.InDelta(t, 0, findMetric(t, r.sent, "unifi_mbb_coverage", labels).value, 0.001)
}
func TestExportUMBBUnadoptedIsSkipped(t *testing.T) {