diff --git a/README.md b/README.md index 25b4517..8f150be 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Create a user on the device that has API and read-only access. `/user group add name=prometheus policy=api,read,winbox` +If `lte` is enabled it requires also the `test` policy. + +`/user group add name=prometheus policy=api,read,winbox,test` + Create the user to access the API via. `/user add name=prometheus group=prometheus password=changeme` diff --git a/collector/collector.go b/collector/collector.go index 22a1b28..48747b2 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -179,6 +179,11 @@ func WithIpsec() Option { func WithConntrack() Option { return func(c *collector) { c.collectors = append(c.collectors, newConntrackCollector()) + +// WithLte enables lte metrics +func WithLte() Option { + return func(c *collector) { + c.collectors = append(c.collectors, newLteCollector()) } } diff --git a/collector/lte_collector.go b/collector/lte_collector.go new file mode 100644 index 0000000..56972d5 --- /dev/null +++ b/collector/lte_collector.go @@ -0,0 +1,111 @@ +package collector + +import ( + "fmt" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/routeros.v2/proto" +) + +type lteCollector struct { + props []string + descriptions map[string]*prometheus.Desc +} + +func newLteCollector() routerOSCollector { + c := <eCollector{} + c.init() + return c +} + +func (c *lteCollector) init() { + c.props = []string{"current-cellid", "rssi", "rsrp", "rsrq", "sinr"} + labelNames := []string{"name", "address", "interface", "cellid"} + c.descriptions = make(map[string]*prometheus.Desc) + for _, p := range c.props { + c.descriptions[p] = descriptionForPropertyName("lte_interface", p, labelNames) + } +} + +func (c *lteCollector) describe(ch chan<- *prometheus.Desc) { + for _, d := range c.descriptions { + ch <- d + } +} + +func (c *lteCollector) 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 *lteCollector) fetchInterfaceNames(ctx *collectorContext) ([]string, error) { + reply, err := ctx.client.Run("/interface/lte/print", "?disabled=false", "=.proplist=name") + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching lte interface names") + return nil, err + } + + names := []string{} + for _, re := range reply.Re { + names = append(names, re.Map["name"]) + } + + return names, nil +} + +func (c *lteCollector) collectForInterface(iface string, ctx *collectorContext) error { + reply, err := ctx.client.Run("/interface/lte/info", fmt.Sprintf("=number=%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 *lteCollector) collectMetricForProperty(property, iface string, re *proto.Sentence, ctx *collectorContext) { + desc := c.descriptions[property] + current_cellid := re.Map["current-cellid"] + if re.Map[property] == "" { + return + } + 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, current_cellid) +} diff --git a/config/config.go b/config/config.go index e900513..2d5b9bb 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ type Config struct { WlanIF bool `yaml:"wlanif,omitempty"` Monitor bool `yaml:"monitor,omitempty"` Ipsec bool `yaml:"ipsec,omitempty"` + Lte bool `yaml:"lte,omitempty"` } `yaml:"features,omitempty"` } diff --git a/config/config.test.yml b/config/config.test.yml index 905f8c6..ed8c30d 100644 --- a/config/config.test.yml +++ b/config/config.test.yml @@ -20,3 +20,4 @@ features: wlansta: true wlanif: true ipsec: true + lte: true diff --git a/config/config_test.go b/config/config_test.go index ac8caf9..e7cd0de 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -29,6 +29,7 @@ func TestShouldParse(t *testing.T) { assertFeature("WlanSTA", c.Features.WlanSTA, t) assertFeature("WlanIF", c.Features.WlanIF, t) assertFeature("Ipsec", c.Features.Ipsec, t) + assertFeature("Lte", c.Features.Lte, t) } func loadTestFile(t *testing.T) []byte { diff --git a/main.go b/main.go index 17b8743..2c262b4 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ var ( withWlanIF = flag.Bool("with-wlanif", false, "retrieves wlan interface metrics") withMonitor = flag.Bool("with-monitor", false, "retrieves ethernet interface monitor info") withIpsec = flag.Bool("with-ipsec", false, "retrieves ipsec metrics") + withLte = flag.Bool("with-lte", false, "retrieves lte metrics") cfg *config.Config @@ -253,6 +254,10 @@ func collectorOptions() []collector.Option { opts = append(opts, collector.WithConntrack()) } + if *withLte || cfg.Features.Lte { + opts = append(opts, collector.WithLte()) + } + if *timeout != collector.DefaultTimeout { opts = append(opts, collector.WithTimeout(*timeout)) }