From 25911b4e60aa225979075bcd936fe3b0ab1ce0fc Mon Sep 17 00:00:00 2001 From: "Robert S. Gerus" Date: Tue, 11 Sep 2018 05:30:13 +0200 Subject: [PATCH] Add wlan station and interface metrics collection. (#14) * Fix newlines. * Added wlan metrics. * Fix WlanSTA collector - return on errors. --- collector/collector.go | 14 ++++ collector/helper.go | 16 +++++ collector/wlanif_collector.go | 109 +++++++++++++++++++++++++++++ collector/wlansta_collector.go | 111 +++++++++++++++++++++++++++++ config/config.go | 92 ++++++++++++------------ config/config_test.go | 124 +++++++++++++++++---------------- config/test/config.test.yml | 40 ++++++----- main.go | 10 +++ 8 files changed, 391 insertions(+), 125 deletions(-) create mode 100644 collector/wlanif_collector.go create mode 100644 collector/wlansta_collector.go diff --git a/collector/collector.go b/collector/collector.go index 30775a8..d9b4d07 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -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) { diff --git a/collector/helper.go b/collector/helper.go index 60784bb..ca486c0 100644 --- a/collector/helper.go +++ b/collector/helper.go @@ -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 +} diff --git a/collector/wlanif_collector.go b/collector/wlanif_collector.go new file mode 100644 index 0000000..08c68a2 --- /dev/null +++ b/collector/wlanif_collector.go @@ -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) +} diff --git a/collector/wlansta_collector.go b/collector/wlansta_collector.go new file mode 100644 index 0000000..e76d7d7 --- /dev/null +++ b/collector/wlansta_collector.go @@ -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) +} diff --git a/config/config.go b/config/config.go index b7ec9fe..387d6a3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,45 +1,47 @@ -package config - -import ( - "io" - "io/ioutil" - - yaml "gopkg.in/yaml.v2" -) - -// Config represents the configuration for the exporter -type Config struct { - Devices []Device `yaml:"devices"` - Features struct { - BGP bool `yaml:"bgp,omitempty"` - DHCP bool `yaml:"dhcp,omitempty"` - DHCPv6 bool `yaml:"dhcpv6,omitempty"` - Routes bool `yaml:"routes,omitempty"` - Pools bool `yaml:"pools,omitempty"` - Optics bool `yaml:"optics,omitempty"` - } `yaml:"features,omitempty"` -} - -// Device represents a target device -type Device struct { - Name string `yaml:"name"` - Address string `yaml:"address"` - User string `yaml:"user"` - Password string `yaml:"password"` -} - -// Load reads YAML from reader and unmashals in Config -func Load(r io.Reader) (*Config, error) { - b, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - - c := &Config{} - err = yaml.Unmarshal(b, c) - if err != nil { - return nil, err - } - - return c, nil -} +package config + +import ( + "io" + "io/ioutil" + + yaml "gopkg.in/yaml.v2" +) + +// Config represents the configuration for the exporter +type Config struct { + Devices []Device `yaml:"devices"` + Features struct { + BGP bool `yaml:"bgp,omitempty"` + DHCP bool `yaml:"dhcp,omitempty"` + DHCPv6 bool `yaml:"dhcpv6,omitempty"` + 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"` +} + +// Device represents a target device +type Device struct { + Name string `yaml:"name"` + Address string `yaml:"address"` + User string `yaml:"user"` + Password string `yaml:"password"` +} + +// Load reads YAML from reader and unmashals in Config +func Load(r io.Reader) (*Config, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + c := &Config{} + err = yaml.Unmarshal(b, c) + if err != nil { + return nil, err + } + + return c, nil +} diff --git a/config/config_test.go b/config/config_test.go index 7aaf1d2..2071e00 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,61 +1,63 @@ -package config - -import ( - "bytes" - "io/ioutil" - "testing" -) - -func TestShouldParse(t *testing.T) { - b := loadTestFile(t) - c, err := Load(bytes.NewReader(b)) - if err != nil { - t.Fatalf("could not parse: %v", err) - } - - if len(c.Devices) != 2 { - t.Fatalf("expected 2 devices, got %v", len(c.Devices)) - } - - assertDevice("test1", "192.168.1.1", "foo", "bar", c.Devices[0], t) - assertDevice("test2", "192.168.2.1", "test", "123", c.Devices[1], t) - assertFeature("BGP", c.Features.BGP, t) - assertFeature("DHCP", c.Features.DHCP, t) - assertFeature("DHCPv6", c.Features.DHCPv6, t) - assertFeature("Pools", c.Features.Pools, t) - assertFeature("Routes", c.Features.Routes, t) - assertFeature("Optics", c.Features.Optics, t) -} - -func loadTestFile(t *testing.T) []byte { - b, err := ioutil.ReadFile("test/config.test.yml") - if err != nil { - t.Fatalf("could not load config: %v", err) - } - - return b -} - -func assertDevice(name, address, user, password string, c Device, t *testing.T) { - if c.Name != name { - t.Fatalf("expected name %s, got %s", name, c.Name) - } - - if c.Address != address { - t.Fatalf("expected address %s, got %s", address, c.Address) - } - - if c.User != user { - t.Fatalf("expected user %s, got %s", user, c.User) - } - - if c.Password != password { - t.Fatalf("expected password %s, got %s", password, c.Password) - } -} - -func assertFeature(name string, v bool, t *testing.T) { - if !v { - t.Fatalf("exprected feature %s to be enabled", name) - } -} +package config + +import ( + "bytes" + "io/ioutil" + "testing" +) + +func TestShouldParse(t *testing.T) { + b := loadTestFile(t) + c, err := Load(bytes.NewReader(b)) + if err != nil { + t.Fatalf("could not parse: %v", err) + } + + if len(c.Devices) != 2 { + t.Fatalf("expected 2 devices, got %v", len(c.Devices)) + } + + assertDevice("test1", "192.168.1.1", "foo", "bar", c.Devices[0], t) + assertDevice("test2", "192.168.2.1", "test", "123", c.Devices[1], t) + assertFeature("BGP", c.Features.BGP, t) + assertFeature("DHCP", c.Features.DHCP, t) + assertFeature("DHCPv6", c.Features.DHCPv6, 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 { + b, err := ioutil.ReadFile("test/config.test.yml") + if err != nil { + t.Fatalf("could not load config: %v", err) + } + + return b +} + +func assertDevice(name, address, user, password string, c Device, t *testing.T) { + if c.Name != name { + t.Fatalf("expected name %s, got %s", name, c.Name) + } + + if c.Address != address { + t.Fatalf("expected address %s, got %s", address, c.Address) + } + + if c.User != user { + t.Fatalf("expected user %s, got %s", user, c.User) + } + + if c.Password != password { + t.Fatalf("expected password %s, got %s", password, c.Password) + } +} + +func assertFeature(name string, v bool, t *testing.T) { + if !v { + t.Fatalf("exprected feature %s to be enabled", name) + } +} diff --git a/config/test/config.test.yml b/config/test/config.test.yml index b148b28..ee2ba66 100644 --- a/config/test/config.test.yml +++ b/config/test/config.test.yml @@ -1,19 +1,21 @@ ---- - -devices: - - name: test1 - address: 192.168.1.1 - user: foo - password: bar - - name: test2 - address: 192.168.2.1 - user: test - password: 123 - -features: - bgp: true - dhcp: true - dhcpv6: true - routes: true - pools: true - optics: true \ No newline at end of file +--- + +devices: + - name: test1 + address: 192.168.1.1 + user: foo + password: bar + - name: test2 + address: 192.168.2.1 + user: test + password: 123 + +features: + bgp: true + dhcp: true + dhcpv6: true + routes: true + pools: true + optics: true + wlansta: true + wlanif: true diff --git a/main.go b/main.go index 36eae7d..b4b3ba4 100644 --- a/main.go +++ b/main.go @@ -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)) }