Exporter for machine metrics
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

406 lines
11 KiB

// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !nowifi
// +build !nowifi
package collector
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"github.com/alecthomas/kingpin/v2"
"github.com/mdlayher/wifi"
"github.com/prometheus/client_golang/prometheus"
)
type wifiCollector struct {
interfaceFrequencyHertz *prometheus.Desc
stationInfo *prometheus.Desc
stationConnectedSecondsTotal *prometheus.Desc
stationInactiveSeconds *prometheus.Desc
stationReceiveBitsPerSecond *prometheus.Desc
stationTransmitBitsPerSecond *prometheus.Desc
stationReceiveBytesTotal *prometheus.Desc
stationTransmitBytesTotal *prometheus.Desc
stationSignalDBM *prometheus.Desc
stationTransmitRetriesTotal *prometheus.Desc
stationTransmitFailedTotal *prometheus.Desc
stationBeaconLossTotal *prometheus.Desc
logger *slog.Logger
}
var (
collectorWifi = kingpin.Flag("collector.wifi.fixtures", "test fixtures to use for wifi collector metrics").Default("").String()
)
func init() {
registerCollector("wifi", defaultDisabled, NewWifiCollector)
}
var _ wifiStater = &wifi.Client{}
// wifiStater is an interface used to swap out a *wifi.Client for end to end tests.
type wifiStater interface {
BSS(ifi *wifi.Interface) (*wifi.BSS, error)
Close() error
Interfaces() ([]*wifi.Interface, error)
StationInfo(ifi *wifi.Interface) ([]*wifi.StationInfo, error)
}
// NewWifiCollector returns a new Collector exposing Wifi statistics.
func NewWifiCollector(logger *slog.Logger) (Collector, error) {
const (
subsystem = "wifi"
)
var (
labels = []string{"device", "mac_address"}
)
return &wifiCollector{
interfaceFrequencyHertz: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "interface_frequency_hertz"),
"The current frequency a WiFi interface is operating at, in hertz.",
[]string{"device"},
nil,
),
stationInfo: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_info"),
"Labeled WiFi interface station information as provided by the operating system.",
[]string{"device", "bssid", "ssid", "mode"},
nil,
),
stationConnectedSecondsTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_connected_seconds_total"),
"The total number of seconds a station has been connected to an access point.",
labels,
nil,
),
stationInactiveSeconds: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_inactive_seconds"),
"The number of seconds since any wireless activity has occurred on a station.",
labels,
nil,
),
stationReceiveBitsPerSecond: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_receive_bits_per_second"),
"The current WiFi receive bitrate of a station, in bits per second.",
labels,
nil,
),
stationTransmitBitsPerSecond: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_transmit_bits_per_second"),
"The current WiFi transmit bitrate of a station, in bits per second.",
labels,
nil,
),
stationReceiveBytesTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_receive_bytes_total"),
"The total number of bytes received by a WiFi station.",
labels,
nil,
),
stationTransmitBytesTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_transmit_bytes_total"),
"The total number of bytes transmitted by a WiFi station.",
labels,
nil,
),
stationSignalDBM: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_signal_dbm"),
"The current WiFi signal strength, in decibel-milliwatts (dBm).",
labels,
nil,
),
stationTransmitRetriesTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_transmit_retries_total"),
"The total number of times a station has had to retry while sending a packet.",
labels,
nil,
),
stationTransmitFailedTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_transmit_failed_total"),
"The total number of times a station has failed to send a packet.",
labels,
nil,
),
stationBeaconLossTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "station_beacon_loss_total"),
"The total number of times a station has detected a beacon loss.",
labels,
nil,
),
logger: logger,
}, nil
}
func (c *wifiCollector) Update(ch chan<- prometheus.Metric) error {
stat, err := newWifiStater(*collectorWifi)
if err != nil {
// Cannot access wifi metrics, report no error.
if errors.Is(err, os.ErrNotExist) {
c.logger.Debug("wifi collector metrics are not available for this system")
return ErrNoData
}
if errors.Is(err, os.ErrPermission) {
c.logger.Debug("wifi collector got permission denied when accessing metrics")
return ErrNoData
}
return fmt.Errorf("failed to access wifi data: %w", err)
}
defer stat.Close()
ifis, err := stat.Interfaces()
if err != nil {
return fmt.Errorf("failed to retrieve wifi interfaces: %w", err)
}
for _, ifi := range ifis {
// Some virtual devices have no "name" and should be skipped.
if ifi.Name == "" {
continue
}
c.logger.Debug("probing wifi device with type", "wifi", ifi.Name, "type", ifi.Type)
ch <- prometheus.MustNewConstMetric(
c.interfaceFrequencyHertz,
prometheus.GaugeValue,
mHzToHz(ifi.Frequency),
ifi.Name,
)
// When a statistic is not available for a given interface, package wifi
// returns a os.ErrNotExist error. We leverage this to only export
// metrics which are actually valid for given interface types.
bss, err := stat.BSS(ifi)
switch {
case err == nil:
c.updateBSSStats(ch, ifi.Name, bss)
case errors.Is(err, os.ErrNotExist):
c.logger.Debug("BSS information not found for wifi device", "name", ifi.Name)
default:
return fmt.Errorf("failed to retrieve BSS for device %s: %v",
ifi.Name, err)
}
stations, err := stat.StationInfo(ifi)
switch {
case err == nil:
for _, station := range stations {
c.updateStationStats(ch, ifi.Name, station)
}
case errors.Is(err, os.ErrNotExist):
c.logger.Debug("station information not found for wifi device", "name", ifi.Name)
default:
return fmt.Errorf("failed to retrieve station info for device %q: %v",
ifi.Name, err)
}
}
return nil
}
func (c *wifiCollector) updateBSSStats(ch chan<- prometheus.Metric, device string, bss *wifi.BSS) {
// Synthetic metric which provides wifi station info, such as SSID, BSSID, etc.
ch <- prometheus.MustNewConstMetric(
c.stationInfo,
prometheus.GaugeValue,
1,
device,
bss.BSSID.String(),
bss.SSID,
bssStatusMode(bss.Status),
)
}
func (c *wifiCollector) updateStationStats(ch chan<- prometheus.Metric, device string, info *wifi.StationInfo) {
ch <- prometheus.MustNewConstMetric(
c.stationConnectedSecondsTotal,
prometheus.CounterValue,
info.Connected.Seconds(),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationInactiveSeconds,
prometheus.GaugeValue,
info.Inactive.Seconds(),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationReceiveBitsPerSecond,
prometheus.GaugeValue,
float64(info.ReceiveBitrate),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationTransmitBitsPerSecond,
prometheus.GaugeValue,
float64(info.TransmitBitrate),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationReceiveBytesTotal,
prometheus.CounterValue,
float64(info.ReceivedBytes),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationTransmitBytesTotal,
prometheus.CounterValue,
float64(info.TransmittedBytes),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationSignalDBM,
prometheus.GaugeValue,
float64(info.Signal),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationTransmitRetriesTotal,
prometheus.CounterValue,
float64(info.TransmitRetries),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationTransmitFailedTotal,
prometheus.CounterValue,
float64(info.TransmitFailed),
device,
info.HardwareAddr.String(),
)
ch <- prometheus.MustNewConstMetric(
c.stationBeaconLossTotal,
prometheus.CounterValue,
float64(info.BeaconLoss),
device,
info.HardwareAddr.String(),
)
}
func mHzToHz(mHz int) float64 {
return float64(mHz) * 1000 * 1000
}
func bssStatusMode(status wifi.BSSStatus) string {
switch status {
case wifi.BSSStatusAuthenticated, wifi.BSSStatusAssociated:
return "client"
case wifi.BSSStatusIBSSJoined:
return "ad-hoc"
default:
return "unknown"
}
}
// All code below this point is used to assist with end-to-end tests for
// the wifi collector, since wifi devices are not available in CI.
// newWifiStater determines if mocked test fixtures from files should be used for
// collecting wifi metrics, or if package wifi should be used.
func newWifiStater(fixtures string) (wifiStater, error) {
if fixtures != "" {
return &mockWifiStater{
fixtures: fixtures,
}, nil
}
return wifi.New()
}
var _ wifiStater = &mockWifiStater{}
type mockWifiStater struct {
fixtures string
}
func (s *mockWifiStater) unmarshalJSONFile(filename string, v interface{}) error {
b, err := os.ReadFile(filepath.Join(s.fixtures, filename))
if err != nil {
return err
}
return json.Unmarshal(b, v)
}
func (s *mockWifiStater) Close() error { return nil }
func (s *mockWifiStater) BSS(ifi *wifi.Interface) (*wifi.BSS, error) {
p := filepath.Join(ifi.Name, "bss.json")
var bss wifi.BSS
if err := s.unmarshalJSONFile(p, &bss); err != nil {
return nil, err
}
return &bss, nil
}
func (s *mockWifiStater) Interfaces() ([]*wifi.Interface, error) {
var ifis []*wifi.Interface
if err := s.unmarshalJSONFile("interfaces.json", &ifis); err != nil {
return nil, err
}
return ifis, nil
}
func (s *mockWifiStater) StationInfo(ifi *wifi.Interface) ([]*wifi.StationInfo, error) {
p := filepath.Join(ifi.Name, "stationinfo.json")
var stations []*wifi.StationInfo
if err := s.unmarshalJSONFile(p, &stations); err != nil {
return nil, err
}
return stations, nil
}