From bcce1fd63ed5e2126243d2ebd30dd5e60eb99f0f Mon Sep 17 00:00:00 2001 From: Reinhard Tartler Date: Sat, 14 Oct 2023 19:14:23 -0400 Subject: [PATCH 1/2] Support wireless metrics for `wifiwave2` devices (#155) from https://forum.mikrotik.com/viewtopic.php?t=195124#p999722: wifiwave2 is an implementation of drivers from the manufacturer of the chipset, rather than an in-house written driver (which wireless is). So there are many small details that are missing or incomplete... wifiwave2 has a slightly different API and has fewer properties to query. This means that not all metrics are available for wifiwave2 devices. Also, the implementation aims at keeping the name metric names for wifiwave2 devices to ensure that existing dashboard continue to work without any modification. --- README.md | 7 ++++ collector/wlanif_collector.go | 16 +++++++- collector/wlansta_collector.go | 75 +++++++++++++++++++++++++++++----- config/config.go | 13 +++--- 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8f150be2..4f470ac6 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ devices: port: 8999 user: prometheus2 password: password_to_second_router + wifiwave2: true - name: routers_srv_dns srv: record: _mikrotik._udp.example.com @@ -87,12 +88,18 @@ features: routes: true pools: true optics: true + wlanif: true + wlansta: true ``` If you add a devices with the `srv` parameter instead of `address` the exporter will perform a DNS query to obtain the SRV record and discover the devices dynamically. Also, you can specify a DNS server to use on the query. +Use the option `wifiwave2: true` for devices that have the `wifiwave2` package, +which replaces the `wireless` implementation, installed. This is necessary as `wifiwave2` has a slightly +different API and exposes a slightly smaller set of attributes (for example, no signal-to-noise, etc.) + ###### example output diff --git a/collector/wlanif_collector.go b/collector/wlanif_collector.go index 491d1c78..4869113a 100644 --- a/collector/wlanif_collector.go +++ b/collector/wlanif_collector.go @@ -53,7 +53,13 @@ func (c *wlanIFCollector) collect(ctx *collectorContext) error { } func (c *wlanIFCollector) fetchInterfaceNames(ctx *collectorContext) ([]string, error) { - reply, err := ctx.client.Run("/interface/wireless/print", "?disabled=false", "=.proplist=name") + cmd := "" + if ctx.device.Wifiwave2 { + cmd = "/interface/wifiwave/print" + } else { + cmd = "/interface/wireless/print" + } + reply, err := ctx.client.Run(cmd, "?disabled=false", "=.proplist=name") if err != nil { log.WithFields(log.Fields{ "device": ctx.device.Name, @@ -71,7 +77,13 @@ func (c *wlanIFCollector) fetchInterfaceNames(ctx *collectorContext) ([]string, } func (c *wlanIFCollector) collectForInterface(iface string, ctx *collectorContext) error { - reply, err := ctx.client.Run("/interface/wireless/monitor", fmt.Sprintf("=numbers=%s", iface), "=once=", "=.proplist="+strings.Join(c.props, ",")) + cmd := "" + if ctx.device.Wifiwave2 { + cmd = "/interface/wifiwave/monitor" + } else { + cmd = "/interface/wireless/monitor" + } + reply, err := ctx.client.Run(cmd, fmt.Sprintf("=numbers=%s", iface), "=once=", "=.proplist="+strings.Join(c.props, ",")) if err != nil { log.WithFields(log.Fields{ "interface": iface, diff --git a/collector/wlansta_collector.go b/collector/wlansta_collector.go index 04715716..cab7cd3f 100644 --- a/collector/wlansta_collector.go +++ b/collector/wlansta_collector.go @@ -9,9 +9,20 @@ import ( "gopkg.in/routeros.v2/proto" ) +// from https://forum.mikrotik.com/viewtopic.php?t=195124#p999722: +// wifiwave2 is an implementation of drivers from the manufacturer of the +// chipset, rather than an in-house written driver (which wireless is). So +// there are many small details that are missing or incomplete... + type wlanSTACollector struct { - props []string - descriptions map[string]*prometheus.Desc + // Both wifiwave2 and wireless have a similar, yet different API. They also + // expose a slightly different set of properties. + props []string + propsWirelessExtra []string + propsWirelessRXTX []string + propsWifiwave2Extra []string + propsWifiwave2RXTX []string + descriptions map[string]*prometheus.Desc } func newWlanSTACollector() routerOSCollector { @@ -21,13 +32,28 @@ func newWlanSTACollector() routerOSCollector { } func (c *wlanSTACollector) init() { - c.props = []string{"interface", "mac-address", "signal-to-noise", "signal-strength", "packets", "bytes", "frames"} + // common properties + c.props = []string{"interface", "mac-address"} + // wifiwave2 doesn't expose SNR, and uses different name for signal-strength + c.propsWirelessExtra = []string{"signal-to-noise", "signal-strength"} + // wireless exposes extra field "frames", not available in wifiwave2 + c.propsWirelessRXTX = []string{"packets", "bytes", "frames"} + c.propsWifiwave2Extra = []string{"signal"} + c.propsWifiwave2RXTX = []string{"packets", "bytes"} + // all metrics have the same label names labelNames := []string{"name", "address", "interface", "mac_address"} c.descriptions = make(map[string]*prometheus.Desc) - for _, p := range c.props[:len(c.props)-3] { + for _, p := range c.propsWirelessExtra { c.descriptions[p] = descriptionForPropertyName("wlan_station", p, labelNames) } - for _, p := range c.props[len(c.props)-3:] { + // normalize the metric name 'signal-strength' for the property "signal", so that dashboards + // that capture both wireless and wifiwave2 devices don't need to normalize + c.descriptions["signal"] = descriptionForPropertyName("wlan_station", "signal-strength", labelNames) + for _, p := range c.propsWirelessRXTX { + c.descriptions["tx_"+p] = descriptionForPropertyName("wlan_station", "tx_"+p, labelNames) + c.descriptions["rx_"+p] = descriptionForPropertyName("wlan_station", "rx_"+p, labelNames) + } + for _, p := range c.propsWifiwave2RXTX { c.descriptions["tx_"+p] = descriptionForPropertyName("wlan_station", "tx_"+p, labelNames) c.descriptions["rx_"+p] = descriptionForPropertyName("wlan_station", "rx_"+p, labelNames) } @@ -53,7 +79,25 @@ func (c *wlanSTACollector) collect(ctx *collectorContext) error { } func (c *wlanSTACollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) { - reply, err := ctx.client.Run("/interface/wireless/registration-table/print", "=.proplist="+strings.Join(c.props, ",")) + var cmd []string + var props []string = c.props + if ctx.device.Wifiwave2 { + props = append(props, c.propsWifiwave2Extra...) + props = append(props, c.propsWifiwave2RXTX...) + cmd = []string{ + "/interface/wifiwave2/registration-table/print", + "=.proplist=" + strings.Join(props, ","), + } + } else { + props = append(props, c.propsWirelessExtra...) + props = append(props, c.propsWirelessRXTX...) + cmd = []string{ + "/interface/wireless/registration-table/print", + "=.proplist=" + strings.Join(props, ","), + } + } + log.Debugf("Running collector command: %s", cmd) + reply, err := ctx.client.Run(cmd...) if err != nil { log.WithFields(log.Fields{ "device": ctx.device.Name, @@ -69,11 +113,20 @@ func (c *wlanSTACollector) collectForStat(re *proto.Sentence, ctx *collectorCont iface := re.Map["interface"] mac := re.Map["mac-address"] - for _, p := range c.props[2 : len(c.props)-3] { - c.collectMetricForProperty(p, iface, mac, re, ctx) - } - for _, p := range c.props[len(c.props)-3:] { - c.collectMetricForTXRXCounters(p, iface, mac, re, ctx) + if ctx.device.Wifiwave2 { + for _, p := range c.propsWifiwave2Extra { + c.collectMetricForProperty(p, iface, mac, re, ctx) + } + for _, p := range c.propsWifiwave2RXTX { + c.collectMetricForTXRXCounters(p, iface, mac, re, ctx) + } + } else { + for _, p := range c.propsWirelessExtra { + c.collectMetricForProperty(p, iface, mac, re, ctx) + } + for _, p := range c.propsWirelessRXTX { + c.collectMetricForTXRXCounters(p, iface, mac, re, ctx) + } } } diff --git a/config/config.go b/config/config.go index 7905d256..6ae1488f 100644 --- a/config/config.go +++ b/config/config.go @@ -35,12 +35,13 @@ type Config struct { // Device represents a target device type Device struct { - Name string `yaml:"name"` - Address string `yaml:"address,omitempty"` - Srv SrvRecord `yaml:"srv,omitempty"` - User string `yaml:"user"` - Password string `yaml:"password"` - Port string `yaml:"port"` + Name string `yaml:"name"` + Address string `yaml:"address,omitempty"` + Srv SrvRecord `yaml:"srv,omitempty"` + User string `yaml:"user"` + Password string `yaml:"password"` + Port string `yaml:"port"` + Wifiwave2 bool `yaml:"wifiwave2"` } type SrvRecord struct { From 4f62adb26d96ba562528edfe4be380905f40f35f Mon Sep 17 00:00:00 2001 From: Reinhard Tartler Date: Sat, 14 Oct 2023 20:49:14 -0400 Subject: [PATCH 2/2] Fix metrics for registered clients --- collector/wlanif_collector.go | 16 ++++++++++++---- collector/wlansta_collector.go | 1 - 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/collector/wlanif_collector.go b/collector/wlanif_collector.go index 4869113a..7167ec0d 100644 --- a/collector/wlanif_collector.go +++ b/collector/wlanif_collector.go @@ -11,8 +11,9 @@ import ( ) type wlanIFCollector struct { - props []string - descriptions map[string]*prometheus.Desc + props []string + propsWifiwave2 []string + descriptions map[string]*prometheus.Desc } func newWlanIFCollector() routerOSCollector { @@ -23,11 +24,15 @@ func newWlanIFCollector() routerOSCollector { func (c *wlanIFCollector) init() { c.props = []string{"channel", "registered-clients", "noise-floor", "overall-tx-ccq"} + // wifiwave2 has slightly different names + c.propsWifiwave2 = []string{"channel", "registered-peers"} labelNames := []string{"name", "address", "interface", "channel"} c.descriptions = make(map[string]*prometheus.Desc) for _, p := range c.props { c.descriptions[p] = descriptionForPropertyName("wlan_interface", p, labelNames) } + // add description for wifiwave2-specific properties to map to wireless ones + c.descriptions["registered-peers"] = descriptionForPropertyName("wlan_interface", "registered-clients", labelNames) } func (c *wlanIFCollector) describe(ch chan<- *prometheus.Desc) { @@ -78,12 +83,15 @@ func (c *wlanIFCollector) fetchInterfaceNames(ctx *collectorContext) ([]string, func (c *wlanIFCollector) collectForInterface(iface string, ctx *collectorContext) error { cmd := "" + var props []string if ctx.device.Wifiwave2 { cmd = "/interface/wifiwave/monitor" + props = c.propsWifiwave2 } else { cmd = "/interface/wireless/monitor" + props = c.props } - reply, err := ctx.client.Run(cmd, fmt.Sprintf("=numbers=%s", iface), "=once=", "=.proplist="+strings.Join(c.props, ",")) + reply, err := ctx.client.Run(cmd, fmt.Sprintf("=numbers=%s", iface), "=once=", "=.proplist="+strings.Join(props, ",")) if err != nil { log.WithFields(log.Fields{ "interface": iface, @@ -93,7 +101,7 @@ func (c *wlanIFCollector) collectForInterface(iface string, ctx *collectorContex return err } - for _, p := range c.props[1:] { + for _, p := range props[1:] { // there's always going to be only one sentence in reply, as we // have to explicitly specify the interface c.collectMetricForProperty(p, iface, reply.Re[0], ctx) diff --git a/collector/wlansta_collector.go b/collector/wlansta_collector.go index cab7cd3f..411af8ab 100644 --- a/collector/wlansta_collector.go +++ b/collector/wlansta_collector.go @@ -96,7 +96,6 @@ func (c *wlanSTACollector) fetch(ctx *collectorContext) ([]*proto.Sentence, erro "=.proplist=" + strings.Join(props, ","), } } - log.Debugf("Running collector command: %s", cmd) reply, err := ctx.client.Run(cmd...) if err != nil { log.WithFields(log.Fields{