From cd1f23db508cf55ac5c8c6fc8f9342f5125282a3 Mon Sep 17 00:00:00 2001 From: Bouke van der Bijl Date: Wed, 17 Sep 2025 15:55:45 +0200 Subject: [PATCH] Collect survey info in wifi collector Signed-off-by: Bouke van der Bijl --- collector/fixtures/e2e-64k-page-output.txt | 40 ++++ collector/fixtures/e2e-output.txt | 40 ++++ collector/fixtures/wifi/wlan0/surveyinfo.json | 30 +++ collector/wifi_linux.go | 195 ++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 collector/fixtures/wifi/wlan0/surveyinfo.json diff --git a/collector/fixtures/e2e-64k-page-output.txt b/collector/fixtures/e2e-64k-page-output.txt index 1914288ef8..c6793ab735 100644 --- a/collector/fixtures/e2e-64k-page-output.txt +++ b/collector/fixtures/e2e-64k-page-output.txt @@ -3376,6 +3376,46 @@ node_wifi_station_transmit_failed_total{device="wlan0",mac_address="aa:bb:cc:dd: # TYPE node_wifi_station_transmit_retries_total counter node_wifi_station_transmit_retries_total{device="wlan0",mac_address="01:02:03:04:05:06"} 20 node_wifi_station_transmit_retries_total{device="wlan0",mac_address="aa:bb:cc:dd:ee:ff"} 10 +# HELP node_wifi_survey_channel_time_active_seconds The time the radio spent on the channel while it was active in seconds. +# TYPE node_wifi_survey_channel_time_active_seconds counter +node_wifi_survey_channel_time_active_seconds{device="wlan0",frequency="2412"} 80 +node_wifi_survey_channel_time_active_seconds{device="wlan0",frequency="2437"} 40 +# HELP node_wifi_survey_channel_time_bss_rx_seconds The time the radio spent on the channel receiving data from a BSS in seconds. +# TYPE node_wifi_survey_channel_time_bss_rx_seconds counter +node_wifi_survey_channel_time_bss_rx_seconds{device="wlan0",frequency="2412"} 15 +node_wifi_survey_channel_time_bss_rx_seconds{device="wlan0",frequency="2437"} 8 +# HELP node_wifi_survey_channel_time_busy_seconds The time the radio spent on the channel while it was busy in seconds. +# TYPE node_wifi_survey_channel_time_busy_seconds counter +node_wifi_survey_channel_time_busy_seconds{device="wlan0",frequency="2412"} 30 +node_wifi_survey_channel_time_busy_seconds{device="wlan0",frequency="2437"} 15 +# HELP node_wifi_survey_channel_time_ext_busy_seconds The time the radio spent on the channel while it was busy with external traffic in seconds. +# TYPE node_wifi_survey_channel_time_ext_busy_seconds counter +node_wifi_survey_channel_time_ext_busy_seconds{device="wlan0",frequency="2412"} 5 +node_wifi_survey_channel_time_ext_busy_seconds{device="wlan0",frequency="2437"} 2 +# HELP node_wifi_survey_channel_time_rx_seconds The time the radio spent on the channel receiving data in seconds. +# TYPE node_wifi_survey_channel_time_rx_seconds counter +node_wifi_survey_channel_time_rx_seconds{device="wlan0",frequency="2412"} 20 +node_wifi_survey_channel_time_rx_seconds{device="wlan0",frequency="2437"} 10 +# HELP node_wifi_survey_channel_time_scan_seconds The time the radio spent on the channel while it was scanning in seconds. +# TYPE node_wifi_survey_channel_time_scan_seconds counter +node_wifi_survey_channel_time_scan_seconds{device="wlan0",frequency="2412"} 2 +node_wifi_survey_channel_time_scan_seconds{device="wlan0",frequency="2437"} 1 +# HELP node_wifi_survey_channel_time_seconds The time the radio spent on the channel in seconds. +# TYPE node_wifi_survey_channel_time_seconds counter +node_wifi_survey_channel_time_seconds{device="wlan0",frequency="2412"} 120 +node_wifi_survey_channel_time_seconds{device="wlan0",frequency="2437"} 60 +# HELP node_wifi_survey_channel_time_tx_seconds The time the radio spent on the channel transmitting data in seconds. +# TYPE node_wifi_survey_channel_time_tx_seconds counter +node_wifi_survey_channel_time_tx_seconds{device="wlan0",frequency="2412"} 10 +node_wifi_survey_channel_time_tx_seconds{device="wlan0",frequency="2437"} 5 +# HELP node_wifi_survey_in_use Indicates if the channel is currently in use (1 for in use, 0 for not in use). +# TYPE node_wifi_survey_in_use gauge +node_wifi_survey_in_use{device="wlan0",frequency="2412"} 1 +node_wifi_survey_in_use{device="wlan0",frequency="2437"} 0 +# HELP node_wifi_survey_noise_dbm The noise level in decibel-milliwatts (dBm). +# TYPE node_wifi_survey_noise_dbm gauge +node_wifi_survey_noise_dbm{device="wlan0",frequency="2412"} -95 +node_wifi_survey_noise_dbm{device="wlan0",frequency="2437"} -92 # HELP node_xfrm_acquire_error_packets_total State hasn’t been fully acquired before use # TYPE node_xfrm_acquire_error_packets_total counter node_xfrm_acquire_error_packets_total 24532 diff --git a/collector/fixtures/e2e-output.txt b/collector/fixtures/e2e-output.txt index 634386da8b..d7878fcb09 100644 --- a/collector/fixtures/e2e-output.txt +++ b/collector/fixtures/e2e-output.txt @@ -3398,6 +3398,46 @@ node_wifi_station_transmit_failed_total{device="wlan0",mac_address="aa:bb:cc:dd: # TYPE node_wifi_station_transmit_retries_total counter node_wifi_station_transmit_retries_total{device="wlan0",mac_address="01:02:03:04:05:06"} 20 node_wifi_station_transmit_retries_total{device="wlan0",mac_address="aa:bb:cc:dd:ee:ff"} 10 +# HELP node_wifi_survey_channel_time_active_seconds The time the radio spent on the channel while it was active in seconds. +# TYPE node_wifi_survey_channel_time_active_seconds counter +node_wifi_survey_channel_time_active_seconds{device="wlan0",frequency="2412"} 80 +node_wifi_survey_channel_time_active_seconds{device="wlan0",frequency="2437"} 40 +# HELP node_wifi_survey_channel_time_bss_rx_seconds The time the radio spent on the channel receiving data from a BSS in seconds. +# TYPE node_wifi_survey_channel_time_bss_rx_seconds counter +node_wifi_survey_channel_time_bss_rx_seconds{device="wlan0",frequency="2412"} 15 +node_wifi_survey_channel_time_bss_rx_seconds{device="wlan0",frequency="2437"} 8 +# HELP node_wifi_survey_channel_time_busy_seconds The time the radio spent on the channel while it was busy in seconds. +# TYPE node_wifi_survey_channel_time_busy_seconds counter +node_wifi_survey_channel_time_busy_seconds{device="wlan0",frequency="2412"} 30 +node_wifi_survey_channel_time_busy_seconds{device="wlan0",frequency="2437"} 15 +# HELP node_wifi_survey_channel_time_ext_busy_seconds The time the radio spent on the channel while it was busy with external traffic in seconds. +# TYPE node_wifi_survey_channel_time_ext_busy_seconds counter +node_wifi_survey_channel_time_ext_busy_seconds{device="wlan0",frequency="2412"} 5 +node_wifi_survey_channel_time_ext_busy_seconds{device="wlan0",frequency="2437"} 2 +# HELP node_wifi_survey_channel_time_rx_seconds The time the radio spent on the channel receiving data in seconds. +# TYPE node_wifi_survey_channel_time_rx_seconds counter +node_wifi_survey_channel_time_rx_seconds{device="wlan0",frequency="2412"} 20 +node_wifi_survey_channel_time_rx_seconds{device="wlan0",frequency="2437"} 10 +# HELP node_wifi_survey_channel_time_scan_seconds The time the radio spent on the channel while it was scanning in seconds. +# TYPE node_wifi_survey_channel_time_scan_seconds counter +node_wifi_survey_channel_time_scan_seconds{device="wlan0",frequency="2412"} 2 +node_wifi_survey_channel_time_scan_seconds{device="wlan0",frequency="2437"} 1 +# HELP node_wifi_survey_channel_time_seconds The time the radio spent on the channel in seconds. +# TYPE node_wifi_survey_channel_time_seconds counter +node_wifi_survey_channel_time_seconds{device="wlan0",frequency="2412"} 120 +node_wifi_survey_channel_time_seconds{device="wlan0",frequency="2437"} 60 +# HELP node_wifi_survey_channel_time_tx_seconds The time the radio spent on the channel transmitting data in seconds. +# TYPE node_wifi_survey_channel_time_tx_seconds counter +node_wifi_survey_channel_time_tx_seconds{device="wlan0",frequency="2412"} 10 +node_wifi_survey_channel_time_tx_seconds{device="wlan0",frequency="2437"} 5 +# HELP node_wifi_survey_in_use Indicates if the channel is currently in use (1 for in use, 0 for not in use). +# TYPE node_wifi_survey_in_use gauge +node_wifi_survey_in_use{device="wlan0",frequency="2412"} 1 +node_wifi_survey_in_use{device="wlan0",frequency="2437"} 0 +# HELP node_wifi_survey_noise_dbm The noise level in decibel-milliwatts (dBm). +# TYPE node_wifi_survey_noise_dbm gauge +node_wifi_survey_noise_dbm{device="wlan0",frequency="2412"} -95 +node_wifi_survey_noise_dbm{device="wlan0",frequency="2437"} -92 # HELP node_xfrm_acquire_error_packets_total State hasn’t been fully acquired before use # TYPE node_xfrm_acquire_error_packets_total counter node_xfrm_acquire_error_packets_total 24532 diff --git a/collector/fixtures/wifi/wlan0/surveyinfo.json b/collector/fixtures/wifi/wlan0/surveyinfo.json new file mode 100644 index 0000000000..76dc4e1cd6 --- /dev/null +++ b/collector/fixtures/wifi/wlan0/surveyinfo.json @@ -0,0 +1,30 @@ +[ + { + "interfaceindex": 3, + "frequency": 2412, + "noise": -95, + "channeltime": 120000000000, + "channeltimeactive": 80000000000, + "channeltimebusy": 30000000000, + "channeltimeextbusy": 5000000000, + "channeltimebssrx": 15000000000, + "channeltimerx": 20000000000, + "channeltimetx": 10000000000, + "channeltimescan": 2000000000, + "inuse": true + }, + { + "interfaceindex": 3, + "frequency": 2437, + "noise": -92, + "channeltime": 60000000000, + "channeltimeactive": 40000000000, + "channeltimebusy": 15000000000, + "channeltimeextbusy": 2000000000, + "channeltimebssrx": 8000000000, + "channeltimerx": 10000000000, + "channeltimetx": 5000000000, + "channeltimescan": 1000000000, + "inuse": false + } +] diff --git a/collector/wifi_linux.go b/collector/wifi_linux.go index c84cdd71d4..da861e27b3 100644 --- a/collector/wifi_linux.go +++ b/collector/wifi_linux.go @@ -44,6 +44,17 @@ type wifiCollector struct { stationTransmitFailedTotal *prometheus.Desc stationBeaconLossTotal *prometheus.Desc + surveyNoiseDBM *prometheus.Desc + surveyChannelTimeSeconds *prometheus.Desc + surveyChannelTimeActiveSeconds *prometheus.Desc + surveyChannelTimeBusySeconds *prometheus.Desc + surveyChannelTimeExtBusySeconds *prometheus.Desc + surveyChannelTimeBssRxSeconds *prometheus.Desc + surveyChannelTimeRxSeconds *prometheus.Desc + surveyChannelTimeTxSeconds *prometheus.Desc + surveyChannelTimeScanSeconds *prometheus.Desc + surveyInUse *prometheus.Desc + logger *slog.Logger } @@ -63,6 +74,7 @@ type wifiStater interface { Close() error Interfaces() ([]*wifi.Interface, error) StationInfo(ifi *wifi.Interface) ([]*wifi.StationInfo, error) + SurveyInfo(ifi *wifi.Interface) ([]*wifi.SurveyInfo, error) } // NewWifiCollector returns a new Collector exposing Wifi statistics. @@ -159,6 +171,77 @@ func NewWifiCollector(logger *slog.Logger) (Collector, error) { labels, nil, ), + + surveyNoiseDBM: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_noise_dbm"), + "The noise level in decibel-milliwatts (dBm).", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_seconds"), + "The time the radio spent on the channel in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeActiveSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_active_seconds"), + "The time the radio spent on the channel while it was active in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeBusySeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_busy_seconds"), + "The time the radio spent on the channel while it was busy in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeExtBusySeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_ext_busy_seconds"), + "The time the radio spent on the channel while it was busy with external traffic in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeBssRxSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_bss_rx_seconds"), + "The time the radio spent on the channel receiving data from a BSS in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeRxSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_rx_seconds"), + "The time the radio spent on the channel receiving data in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeTxSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_tx_seconds"), + "The time the radio spent on the channel transmitting data in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyChannelTimeScanSeconds: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_channel_time_scan_seconds"), + "The time the radio spent on the channel while it was scanning in seconds.", + []string{"device", "frequency_mhz"}, + nil, + ), + + surveyInUse: prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "survey_in_use"), + "Indicates if the channel is currently in use (1 for in use, 0 for not in use).", + []string{"device", "frequency_mhz"}, + nil, + ), + logger: logger, }, nil } @@ -227,6 +310,19 @@ func (c *wifiCollector) Update(ch chan<- prometheus.Metric) error { return fmt.Errorf("failed to retrieve station info for device %q: %v", ifi.Name, err) } + + surveys, err := stat.SurveyInfo(ifi) + switch { + case err == nil: + for _, survey := range surveys { + c.updateSurveyStats(ch, ifi.Name, survey) + } + case errors.Is(err, os.ErrNotExist): + c.logger.Debug("survey information not found for wifi device", "name", ifi.Name) + default: + return fmt.Errorf("failed to retrieve survey info for device %q: %v", + ifi.Name, err) + } } return nil @@ -327,6 +423,94 @@ func (c *wifiCollector) updateStationStats(ch chan<- prometheus.Metric, device s ) } +func (c *wifiCollector) updateSurveyStats(ch chan<- prometheus.Metric, device string, info *wifi.SurveyInfo) { + frequencyMHz := fmt.Sprintf("%d", info.Frequency) + + ch <- prometheus.MustNewConstMetric( + c.surveyNoiseDBM, + prometheus.GaugeValue, + float64(info.Noise), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeSeconds, + prometheus.CounterValue, + info.ChannelTime.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeActiveSeconds, + prometheus.CounterValue, + info.ChannelTimeActive.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeBusySeconds, + prometheus.CounterValue, + info.ChannelTimeBusy.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeExtBusySeconds, + prometheus.CounterValue, + info.ChannelTimeExtBusy.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeBssRxSeconds, + prometheus.CounterValue, + info.ChannelTimeBssRx.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeRxSeconds, + prometheus.CounterValue, + info.ChannelTimeRx.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeTxSeconds, + prometheus.CounterValue, + info.ChannelTimeTx.Seconds(), + device, + frequencyMHz, + ) + + ch <- prometheus.MustNewConstMetric( + c.surveyChannelTimeScanSeconds, + prometheus.CounterValue, + info.ChannelTimeScan.Seconds(), + device, + frequencyMHz, + ) + + var inUseValue float64 + if info.InUse { + inUseValue = 1 + } + ch <- prometheus.MustNewConstMetric( + c.surveyInUse, + prometheus.GaugeValue, + inUseValue, + device, + frequencyMHz, + ) +} + func mHzToHz(mHz int) float64 { return float64(mHz) * 1000 * 1000 } @@ -404,3 +588,14 @@ func (s *mockWifiStater) StationInfo(ifi *wifi.Interface) ([]*wifi.StationInfo, return stations, nil } + +func (s *mockWifiStater) SurveyInfo(ifi *wifi.Interface) ([]*wifi.SurveyInfo, error) { + p := filepath.Join(ifi.Name, "surveyinfo.json") + + var surveyInfo []*wifi.SurveyInfo + if err := s.unmarshalJSONFile(p, &surveyInfo); err != nil { + return nil, err + } + + return surveyInfo, nil +}