Add wlan station and interface metrics collection. (#14)

* Fix newlines.
* Added wlan metrics.
* Fix WlanSTA collector - return on errors.
This commit is contained in:
Robert S. Gerus 2018-09-11 05:30:13 +02:00 committed by Steve Brunton
parent f6b4764691
commit 25911b4e60
8 changed files with 391 additions and 125 deletions

View File

@ -85,6 +85,20 @@ func WithOptics() Option {
}
}
// WithWlanSTA enables wlan STA metrics
func WithWlanSTA() Option {
return func(c *collector) {
c.collectors = append(c.collectors, newWlanSTACollector())
}
}
// WithWlanIF enables wireless interface metrics
func WithWlanIF() Option {
return func(c *collector) {
c.collectors = append(c.collectors, newWlanIFCollector())
}
}
// WithTimeout sets timeout for connecting to router
func WithTimeout(d time.Duration) Option {
return func(c *collector) {

View File

@ -1,6 +1,8 @@
package collector
import (
"math"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
@ -27,3 +29,17 @@ func description(prefix, name, helpText string, labelNames []string) *prometheus
nil,
)
}
func splitStringToFloats(metric string) (float64, float64, error) {
strings := strings.Split(metric, ",")
m1, err := strconv.ParseFloat(strings[0], 64)
if err != nil {
return math.NaN(), math.NaN(), err
}
m2, err := strconv.ParseFloat(strings[1], 64)
if err != nil {
return math.NaN(), math.NaN(), err
}
return m1, m2, nil
}

View File

@ -0,0 +1,109 @@
package collector
import (
"fmt"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"gopkg.in/routeros.v2/proto"
)
type wlanIFCollector struct {
props []string
descriptions map[string]*prometheus.Desc
}
func newWlanIFCollector() routerOSCollector {
c := &wlanIFCollector{}
c.init()
return c
}
func (c *wlanIFCollector) init() {
c.props = []string{"channel", "registered-clients", "noise-floor", "overall-tx-ccq"}
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)
}
}
func (c *wlanIFCollector) describe(ch chan<- *prometheus.Desc) {
for _, d := range c.descriptions {
ch <- d
}
}
func (c *wlanIFCollector) collect(ctx *collectorContext) error {
names, err := c.fetchInterfaceNames(ctx)
if err != nil {
return err
}
for _, n := range names {
err := c.collectForInterface(n, ctx)
if err != nil {
return err
}
}
return nil
}
func (c *wlanIFCollector) fetchInterfaceNames(ctx *collectorContext) ([]string, error) {
reply, err := ctx.client.Run("/interface/wireless/print", "?disabled=false", "=.proplist=name")
if err != nil {
log.WithFields(log.Fields{
"device": ctx.device.Name,
"error": err,
}).Error("error fetching wireless interface names")
return nil, err
}
names := []string{}
for _, re := range reply.Re {
names = append(names, re.Map["name"])
}
return names, nil
}
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, ","))
if err != nil {
log.WithFields(log.Fields{
"interface": iface,
"device": ctx.device.Name,
"error": err,
}).Error("error fetching interface statistics")
return err
}
for _, p := range c.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)
}
return nil
}
func (c *wlanIFCollector) collectMetricForProperty(property, iface string, re *proto.Sentence, ctx *collectorContext) {
desc := c.descriptions[property]
channel := re.Map["channel"]
v, err := strconv.ParseFloat(re.Map[property], 64)
if err != nil {
log.WithFields(log.Fields{
"property": property,
"interface": iface,
"device": ctx.device.Name,
"error": err,
}).Error("error parsing interface metric value")
return
}
ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, iface, channel)
}

View File

@ -0,0 +1,111 @@
package collector
import (
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"gopkg.in/routeros.v2/proto"
)
type wlanSTACollector struct {
props []string
descriptions map[string]*prometheus.Desc
}
func newWlanSTACollector() routerOSCollector {
c := &wlanSTACollector{}
c.init()
return c
}
func (c *wlanSTACollector) init() {
c.props = []string{"interface", "mac-address", "signal-to-noise", "signal-strength-ch0", "packets", "bytes", "frames"}
labelNames := []string{"name", "address", "interface", "mac_address"}
c.descriptions = make(map[string]*prometheus.Desc)
for _, p := range c.props[:len(c.props)-3] {
c.descriptions[p] = descriptionForPropertyName("wlan_station", p, labelNames)
}
for _, p := range c.props[len(c.props)-3:] {
c.descriptions["tx_"+p] = descriptionForPropertyName("wlan_station", "tx_"+p, labelNames)
c.descriptions["rx_"+p] = descriptionForPropertyName("wlan_station", "rx_"+p, labelNames)
}
}
func (c *wlanSTACollector) describe(ch chan<- *prometheus.Desc) {
for _, d := range c.descriptions {
ch <- d
}
}
func (c *wlanSTACollector) collect(ctx *collectorContext) error {
stats, err := c.fetch(ctx)
if err != nil {
return err
}
for _, re := range stats {
c.collectForStat(re, ctx)
}
return nil
}
func (c *wlanSTACollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) {
reply, err := ctx.client.Run("/interface/wireless/registration-table/print", "=.proplist="+strings.Join(c.props, ","))
if err != nil {
log.WithFields(log.Fields{
"device": ctx.device.Name,
"error": err,
}).Error("error fetching wlan station metrics")
return nil, err
}
return reply.Re, nil
}
func (c *wlanSTACollector) collectForStat(re *proto.Sentence, ctx *collectorContext) {
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)
}
}
func (c *wlanSTACollector) collectMetricForProperty(property, iface, mac string, re *proto.Sentence, ctx *collectorContext) {
v, err := strconv.ParseFloat(re.Map[property], 64)
if err != nil {
log.WithFields(log.Fields{
"device": ctx.device.Name,
"property": property,
"value": re.Map[property],
"error": err,
}).Error("error parsing wlan station metric value")
return
}
desc := c.descriptions[property]
ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, iface, mac)
}
func (c *wlanSTACollector) collectMetricForTXRXCounters(property, iface, mac string, re *proto.Sentence, ctx *collectorContext) {
tx, rx, err := splitStringToFloats(re.Map[property])
if err != nil {
log.WithFields(log.Fields{
"device": ctx.device.Name,
"property": property,
"value": re.Map[property],
"error": err,
}).Error("error parsing wlan station metric value")
return
}
desc_tx := c.descriptions["tx_"+property]
desc_rx := c.descriptions["rx_"+property]
ctx.ch <- prometheus.MustNewConstMetric(desc_tx, prometheus.CounterValue, tx, ctx.device.Name, ctx.device.Address, iface, mac)
ctx.ch <- prometheus.MustNewConstMetric(desc_rx, prometheus.CounterValue, rx, ctx.device.Name, ctx.device.Address, iface, mac)
}

View File

@ -17,6 +17,8 @@ type Config struct {
Routes bool `yaml:"routes,omitempty"`
Pools bool `yaml:"pools,omitempty"`
Optics bool `yaml:"optics,omitempty"`
WlanSTA bool `yaml:"wlansta,omitempty"`
WlanIF bool `yaml:"wlanif,omitempty"`
} `yaml:"features,omitempty"`
}

View File

@ -25,6 +25,8 @@ func TestShouldParse(t *testing.T) {
assertFeature("Pools", c.Features.Pools, t)
assertFeature("Routes", c.Features.Routes, t)
assertFeature("Optics", c.Features.Optics, t)
assertFeature("WlanSTA", c.Features.WlanSTA, t)
assertFeature("WlanIF", c.Features.WlanIF, t)
}
func loadTestFile(t *testing.T) []byte {

View File

@ -17,3 +17,5 @@ features:
routes: true
pools: true
optics: true
wlansta: true
wlanif: true

10
main.go
View File

@ -35,6 +35,8 @@ var (
withDHCPv6 = flag.Bool("with-dhcpv6", false, "retrieves DHCPv6 server metrics")
withPools = flag.Bool("with-pools", false, "retrieves IP(v6) pool metrics")
withOptics = flag.Bool("with-optics", false, "retrieves optical diagnostic metrics")
withWlanSTA = flag.Bool("with-wlansta", false, "retrieves connected wlan station metrics")
withWlanIF = flag.Bool("with-wlanif", false, "retrieves wlan interface metrics")
timeout = flag.Duration("timeout", collector.DefaultTimeout*time.Second, "timeout when connecting to routers")
tls = flag.Bool("tls", false, "use tls to connect to routers")
insecure = flag.Bool("insecure", false, "skips verification of server certificate when using TLS (not recommended)")
@ -175,6 +177,14 @@ func collectorOptions() []collector.Option {
opts = append(opts, collector.WithOptics())
}
if *withWlanSTA || cfg.Features.WlanSTA {
opts = append(opts, collector.WithWlanSTA())
}
if *withWlanIF || cfg.Features.WlanIF {
opts = append(opts, collector.WithWlanIF())
}
if *timeout != collector.DefaultTimeout {
opts = append(opts, collector.WithTimeout(*timeout))
}