From d170b0a4d273831fa28977d2b77f9635508e7b82 Mon Sep 17 00:00:00 2001 From: Daniel Czerwonk Date: Wed, 11 Apr 2018 15:21:38 +0200 Subject: [PATCH] More features (#9) * added config file implementation, refactoring * add gitignore * improved test * preperations for more metrics * added resource metrics * added first bgp metrics * added asn as label for bgp metrics * added prefix and message counts to bgp metrics * simplified * Update README.md * added yaml dependency * fixed go routine call * added timeout * clean up * added TLS support * set default api port for TLS * added routes metric * added missing log information * added type collectorContext to reduce the count of parameters for better readability * added DHCP and DHCPv6 metrics * filter for active dhcp leases only * added pool metrics * enable/disable feature in config file * refactoring * clean up * comment fix --- README.md | 9 +- collector/bgp_collector.go | 69 +++++------ collector/collector.go | 34 +++++- ...tric_collector.go => collector_context.go} | 7 +- collector/dhcp_collector.go | 89 ++++++++++++++ collector/dhcpv6_collector.go | 89 ++++++++++++++ collector/helper.go | 9 ++ collector/interface_collector.go | 60 +++++----- collector/pool_collector.go | 105 ++++++++++++++++ collector/resource_collector.go | 59 ++++----- collector/routeros_collector.go | 10 ++ collector/routes_collector.go | 113 ++++++++++-------- config/config.go | 12 +- config/config_test.go | 17 ++- config/test/config.test.yml | 9 +- main.go | 43 ++++--- 16 files changed, 559 insertions(+), 175 deletions(-) rename collector/{metric_collector.go => collector_context.go} (50%) create mode 100644 collector/dhcp_collector.go create mode 100644 collector/dhcpv6_collector.go create mode 100644 collector/pool_collector.go create mode 100644 collector/routeros_collector.go diff --git a/README.md b/README.md index 11d5a6b..7215da6 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ created for the exporter to use to access the API. where `config-file` is the path to a config file in YAML format. ###### example config -``` +```yaml devices: - name: my_router address: 10.10.0.1 @@ -52,6 +52,13 @@ devices: address: 10.10.0.2 user: prometheus2 password: password_to_second_router + +features: + bgp: true + dhcp: true + dhcpv6: true + routes: true + pools: true ``` ###### example output diff --git a/collector/bgp_collector.go b/collector/bgp_collector.go index 1787e2d..d27caa0 100644 --- a/collector/bgp_collector.go +++ b/collector/bgp_collector.go @@ -4,59 +4,60 @@ import ( "strconv" "strings" - "github.com/nshttpd/mikrotik-exporter/config" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - routeros "gopkg.in/routeros.v2" "gopkg.in/routeros.v2/proto" ) -var ( - bgpabelNames = []string{"name", "address", "session", "asn"} - bgpProps = []string{"name", "remote-as", "state", "prefix-count", "updates-sent", "updates-received", "withdrawn-sent", "withdrawn-received"} - bgpDescriptions map[string]*prometheus.Desc -) +type bgpCollector struct { + props []string + descriptions map[string]*prometheus.Desc +} -func init() { - bgpDescriptions = make(map[string]*prometheus.Desc) - bgpDescriptions["state"] = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "bgp", "up"), - "BGP session is established (up = 1)", - bgpabelNames, - nil, - ) - for _, p := range bgpProps[3:] { - bgpDescriptions[p] = descriptionForPropertyName("bgp", p, bgpabelNames) +func newBGPCollector() routerOSCollector { + c := &bgpCollector{} + c.init() + return c +} + +func (c *bgpCollector) init() { + c.props = []string{"name", "remote-as", "state", "prefix-count", "updates-sent", "updates-received", "withdrawn-sent", "withdrawn-received"} + + const prefix = "bgp" + labelNames := []string{"name", "address", "session", "asn"} + + c.descriptions = make(map[string]*prometheus.Desc) + c.descriptions["state"] = description(prefix, "up", "BGP session is established (up = 1)", labelNames) + + for _, p := range c.props[3:] { + c.descriptions[p] = descriptionForPropertyName(prefix, p, labelNames) } } -type bgpCollector struct { -} - func (c *bgpCollector) describe(ch chan<- *prometheus.Desc) { - for _, d := range bgpDescriptions { + for _, d := range c.descriptions { ch <- d } } -func (c *bgpCollector) collect(ch chan<- prometheus.Metric, device *config.Device, client *routeros.Client) error { - stats, err := c.fetch(client, device) +func (c *bgpCollector) collect(ctx *collectorContext) error { + stats, err := c.fetch(ctx) if err != nil { return err } for _, re := range stats { - c.collectForStat(re, device, ch) + c.collectForStat(re, ctx) } return nil } -func (c *bgpCollector) fetch(client *routeros.Client, device *config.Device) ([]*proto.Sentence, error) { - reply, err := client.Run("/routing/bgp/peer/print", "=.proplist="+strings.Join(bgpProps, ",")) +func (c *bgpCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) { + reply, err := ctx.client.Run("/routing/bgp/peer/print", "=.proplist="+strings.Join(c.props, ",")) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error fetching bgp metrics") return nil, err @@ -65,25 +66,25 @@ func (c *bgpCollector) fetch(client *routeros.Client, device *config.Device) ([] return reply.Re, nil } -func (c *bgpCollector) collectForStat(re *proto.Sentence, device *config.Device, ch chan<- prometheus.Metric) { +func (c *bgpCollector) collectForStat(re *proto.Sentence, ctx *collectorContext) { var session, asn string - for _, p := range bgpProps { + for _, p := range c.props { if p == "name" { session = re.Map[p] } else if p == "remote-as" { asn = re.Map[p] } else { - c.collectMetricForProperty(p, session, asn, device, re, ch) + c.collectMetricForProperty(p, session, asn, re, ctx) } } } -func (c *bgpCollector) collectMetricForProperty(property, session, asn string, device *config.Device, re *proto.Sentence, ch chan<- prometheus.Metric) { - desc := bgpDescriptions[property] +func (c *bgpCollector) collectMetricForProperty(property, session, asn string, re *proto.Sentence, ctx *collectorContext) { + desc := c.descriptions[property] v, err := c.parseValueForProperty(property, re.Map[property]) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "session": session, "property": property, "value": re.Map[property], @@ -92,7 +93,7 @@ func (c *bgpCollector) collectMetricForProperty(property, session, asn string, d return } - ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, v, device.Name, device.Address, session, asn) + ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, session, asn) } func (c *bgpCollector) parseValueForProperty(property, value string) (float64, error) { diff --git a/collector/collector.go b/collector/collector.go index af6b411..d67e37a 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -37,7 +37,7 @@ var ( type collector struct { devices []config.Device - collectors []metricCollector + collectors []routerOSCollector timeout time.Duration enableTLS bool insecureTLS bool @@ -53,7 +53,28 @@ func WithBGP() Option { // WithRoutes enables routing table metrics func WithRoutes() Option { return func(c *collector) { - c.collectors = append(c.collectors, &routesCollector{}) + c.collectors = append(c.collectors, newRoutesCollector()) + } +} + +// WithDHCP enables DHCP serrver metrics +func WithDHCP() Option { + return func(c *collector) { + c.collectors = append(c.collectors, newDHCPCollector()) + } +} + +// WithDHCPv6 enables DHCPv6 serrver metrics +func WithDHCPv6() Option { + return func(c *collector) { + c.collectors = append(c.collectors, newDHCPv6Collector()) + } +} + +// WithPools enables IP(v6) pool metrics +func WithPools() Option { + return func(c *collector) { + c.collectors = append(c.collectors, newPoolCollector()) } } @@ -84,9 +105,9 @@ func NewCollector(cfg *config.Config, opts ...Option) (prometheus.Collector, err c := &collector{ devices: cfg.Devices, timeout: DefaultTimeout, - collectors: []metricCollector{ - &interfaceCollector{}, - &resourceCollector{}, + collectors: []routerOSCollector{ + newInterfaceCollector(), + newResourceCollector(), }, } @@ -153,7 +174,8 @@ func (c *collector) connectAndCollect(d *config.Device, ch chan<- prometheus.Met defer cl.Close() for _, co := range c.collectors { - err = co.collect(ch, d, cl) + ctx := &collectorContext{ch, d, cl} + err = co.collect(ctx) if err != nil { return err } diff --git a/collector/metric_collector.go b/collector/collector_context.go similarity index 50% rename from collector/metric_collector.go rename to collector/collector_context.go index f0fed90..eb29f22 100644 --- a/collector/metric_collector.go +++ b/collector/collector_context.go @@ -6,7 +6,8 @@ import ( routeros "gopkg.in/routeros.v2" ) -type metricCollector interface { - describe(ch chan<- *prometheus.Desc) - collect(ch chan<- prometheus.Metric, device *config.Device, client *routeros.Client) error +type collectorContext struct { + ch chan<- prometheus.Metric + device *config.Device + client *routeros.Client } diff --git a/collector/dhcp_collector.go b/collector/dhcp_collector.go new file mode 100644 index 0000000..94580fd --- /dev/null +++ b/collector/dhcp_collector.go @@ -0,0 +1,89 @@ +package collector + +import ( + "fmt" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +type dhcpCollector struct { + leasesActiveCountDesc *prometheus.Desc +} + +func (c *dhcpCollector) init() { + const prefix = "dhcp" + + labelNames := []string{"name", "address", "server"} + c.leasesActiveCountDesc = description(prefix, "leases_active_count", "number of active leases per DHCP server", labelNames) +} + +func newDHCPCollector() routerOSCollector { + c := &dhcpCollector{} + c.init() + return c +} + +func (c *dhcpCollector) describe(ch chan<- *prometheus.Desc) { + ch <- c.leasesActiveCountDesc +} + +func (c *dhcpCollector) collect(ctx *collectorContext) error { + names, err := c.fetchDHCPServerNames(ctx) + if err != nil { + return err + } + + for _, n := range names { + err := c.colllectForDHCPServer(ctx, n) + if err != nil { + return err + } + } + + return nil +} + +func (c *dhcpCollector) fetchDHCPServerNames(ctx *collectorContext) ([]string, error) { + reply, err := ctx.client.Run("/ip/dhcp-server/print", "=.proplist=name") + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching DHCP server names") + return nil, err + } + + names := []string{} + for _, re := range reply.Re { + names = append(names, re.Map["name"]) + } + + return names, nil +} + +func (c *dhcpCollector) colllectForDHCPServer(ctx *collectorContext, dhcpServer string) error { + reply, err := ctx.client.Run("/ip/dhcp-server/lease/print", fmt.Sprintf("?server=%s", dhcpServer), "=active=", "=count-only=") + if err != nil { + log.WithFields(log.Fields{ + "dhcp_server": dhcpServer, + "device": ctx.device.Name, + "error": err, + }).Error("error fetching DHCP lease counts") + return err + } + + v, err := strconv.ParseFloat(reply.Done.Map["ret"], 32) + if err != nil { + log.WithFields(log.Fields{ + "dhcp_server": dhcpServer, + "device": ctx.device.Name, + "error": err, + }).Error("error parsing DHCP lease counts") + return err + } + + ctx.ch <- prometheus.MustNewConstMetric(c.leasesActiveCountDesc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, dhcpServer) + return nil +} diff --git a/collector/dhcpv6_collector.go b/collector/dhcpv6_collector.go new file mode 100644 index 0000000..f47e550 --- /dev/null +++ b/collector/dhcpv6_collector.go @@ -0,0 +1,89 @@ +package collector + +import ( + "fmt" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +type dhcpv6Collector struct { + bindingCountDesc *prometheus.Desc +} + +func newDHCPv6Collector() routerOSCollector { + c := &dhcpv6Collector{} + c.init() + return c +} + +func (c *dhcpv6Collector) init() { + const prefix = "dhcpv6" + + labelNames := []string{"name", "address", "server"} + c.bindingCountDesc = description(prefix, "binding_count", "number of active bindings per DHCPv6 server", labelNames) +} + +func (c *dhcpv6Collector) describe(ch chan<- *prometheus.Desc) { + ch <- c.bindingCountDesc +} + +func (c *dhcpv6Collector) collect(ctx *collectorContext) error { + names, err := c.fetchDHCPServerNames(ctx) + if err != nil { + return err + } + + for _, n := range names { + err := c.colllectForDHCPServer(ctx, n) + if err != nil { + return err + } + } + + return nil +} + +func (c *dhcpv6Collector) fetchDHCPServerNames(ctx *collectorContext) ([]string, error) { + reply, err := ctx.client.Run("/ipv6/dhcp-server/print", "=.proplist=name") + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching DHCPv6 server names") + return nil, err + } + + names := []string{} + for _, re := range reply.Re { + names = append(names, re.Map["name"]) + } + + return names, nil +} + +func (c *dhcpv6Collector) colllectForDHCPServer(ctx *collectorContext, dhcpServer string) error { + reply, err := ctx.client.Run("/ipv6/dhcp-server/binding/print", fmt.Sprintf("?server=%s", dhcpServer), "=count-only=") + if err != nil { + log.WithFields(log.Fields{ + "dhcpv6_server": dhcpServer, + "device": ctx.device.Name, + "error": err, + }).Error("error fetching DHCPv6 binding counts") + return err + } + + v, err := strconv.ParseFloat(reply.Done.Map["ret"], 32) + if err != nil { + log.WithFields(log.Fields{ + "dhcpv6_server": dhcpServer, + "device": ctx.device.Name, + "error": err, + }).Error("error parsing DHCPv6 binding counts") + return err + } + + ctx.ch <- prometheus.MustNewConstMetric(c.bindingCountDesc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, dhcpServer) + return nil +} diff --git a/collector/helper.go b/collector/helper.go index 861c0a7..60784bb 100644 --- a/collector/helper.go +++ b/collector/helper.go @@ -18,3 +18,12 @@ func descriptionForPropertyName(prefix, property string, labelNames []string) *p nil, ) } + +func description(prefix, name, helpText string, labelNames []string) *prometheus.Desc { + return prometheus.NewDesc( + prometheus.BuildFQName(namespace, prefix, name), + helpText, + labelNames, + nil, + ) +} diff --git a/collector/interface_collector.go b/collector/interface_collector.go index fd195e4..b40e824 100644 --- a/collector/interface_collector.go +++ b/collector/interface_collector.go @@ -4,54 +4,56 @@ import ( "strconv" "strings" - "github.com/nshttpd/mikrotik-exporter/config" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "gopkg.in/routeros.v2" "gopkg.in/routeros.v2/proto" ) -var ( - interfaceLabelNames = []string{"name", "address", "interface"} - interfaceProps = []string{"name", "rx-byte", "tx-byte", "rx-packet", "tx-packet", "rx-error", "tx-error", "rx-drop", "tx-drop"} - interfaceDescriptions map[string]*prometheus.Desc -) +type interfaceCollector struct { + props []string + descriptions map[string]*prometheus.Desc +} -func init() { - interfaceDescriptions = make(map[string]*prometheus.Desc) - for _, p := range interfaceProps[1:] { - interfaceDescriptions[p] = descriptionForPropertyName("interface", p, interfaceLabelNames) +func newInterfaceCollector() routerOSCollector { + c := &interfaceCollector{} + c.init() + return c +} + +func (c *interfaceCollector) init() { + c.props = []string{"name", "rx-byte", "tx-byte", "rx-packet", "tx-packet", "rx-error", "tx-error", "rx-drop", "tx-drop"} + + labelNames := []string{"name", "address", "interface"} + c.descriptions = make(map[string]*prometheus.Desc) + for _, p := range c.props[1:] { + c.descriptions[p] = descriptionForPropertyName("interface", p, labelNames) } } -type interfaceCollector struct { -} - func (c *interfaceCollector) describe(ch chan<- *prometheus.Desc) { - for _, d := range interfaceDescriptions { + for _, d := range c.descriptions { ch <- d } } -func (c *interfaceCollector) collect(ch chan<- prometheus.Metric, device *config.Device, client *routeros.Client) error { - stats, err := c.fetch(client, device) +func (c *interfaceCollector) collect(ctx *collectorContext) error { + stats, err := c.fetch(ctx) if err != nil { return err } for _, re := range stats { - c.collectForStat(re, device, ch) + c.collectForStat(re, ctx) } return nil } -func (c *interfaceCollector) fetch(client *routeros.Client, device *config.Device) ([]*proto.Sentence, error) { - reply, err := client.Run("/interface/print", "?disabled=false", - "?running=true", "=.proplist="+strings.Join(interfaceProps, ",")) +func (c *interfaceCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) { + reply, err := ctx.client.Run("/interface/print", "?disabled=false", "?running=true", "=.proplist="+strings.Join(c.props, ",")) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error fetching interface metrics") return nil, err @@ -60,23 +62,23 @@ func (c *interfaceCollector) fetch(client *routeros.Client, device *config.Devic return reply.Re, nil } -func (c *interfaceCollector) collectForStat(re *proto.Sentence, device *config.Device, ch chan<- prometheus.Metric) { +func (c *interfaceCollector) collectForStat(re *proto.Sentence, ctx *collectorContext) { var iface string - for _, p := range interfaceProps { + for _, p := range c.props { if p == "name" { iface = re.Map[p] } else { - c.collectMetricForProperty(p, iface, device, re, ch) + c.collectMetricForProperty(p, iface, re, ctx) } } } -func (c *interfaceCollector) collectMetricForProperty(property, iface string, device *config.Device, re *proto.Sentence, ch chan<- prometheus.Metric) { - desc := interfaceDescriptions[property] +func (c *interfaceCollector) collectMetricForProperty(property, iface string, re *proto.Sentence, ctx *collectorContext) { + desc := c.descriptions[property] v, err := strconv.ParseFloat(re.Map[property], 64) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "interface": iface, "property": property, "value": re.Map[property], @@ -85,5 +87,5 @@ func (c *interfaceCollector) collectMetricForProperty(property, iface string, de return } - ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, device.Name, device.Address, iface) + ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, ctx.device.Name, ctx.device.Address, iface) } diff --git a/collector/pool_collector.go b/collector/pool_collector.go new file mode 100644 index 0000000..a8efb72 --- /dev/null +++ b/collector/pool_collector.go @@ -0,0 +1,105 @@ +package collector + +import ( + "fmt" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +type poolCollector struct { + usedCountDesc *prometheus.Desc +} + +func (c *poolCollector) init() { + const prefix = "ip_pool" + + labelNames := []string{"name", "address", "ip_version", "pool"} + c.usedCountDesc = description(prefix, "pool_used_count", "number of used IP/prefixes in a pool", labelNames) +} + +func newPoolCollector() routerOSCollector { + c := &poolCollector{} + c.init() + return c +} + +func (c *poolCollector) describe(ch chan<- *prometheus.Desc) { + ch <- c.usedCountDesc +} + +func (c *poolCollector) collect(ctx *collectorContext) error { + err := c.collectForIPVersion("4", "ip", ctx) + if err != nil { + return err + } + + err = c.collectForIPVersion("6", "ipv6", ctx) + if err != nil { + return err + } + + return nil +} + +func (c *poolCollector) collectForIPVersion(ipVersion, topic string, ctx *collectorContext) error { + names, err := c.fetchPoolNames(ipVersion, topic, ctx) + if err != nil { + return err + } + + for _, n := range names { + err := c.collectForPool(ipVersion, topic, n, ctx) + if err != nil { + return err + } + } + + return nil +} + +func (c *poolCollector) fetchPoolNames(ipVersion, topic string, ctx *collectorContext) ([]string, error) { + reply, err := ctx.client.Run(fmt.Sprintf("/%s/pool/print", topic), "=.proplist=name") + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching pool names") + return nil, err + } + + names := []string{} + for _, re := range reply.Re { + names = append(names, re.Map["name"]) + } + + return names, nil +} + +func (c *poolCollector) collectForPool(ipVersion, topic, pool string, ctx *collectorContext) error { + reply, err := ctx.client.Run(fmt.Sprintf("/%s/pool/used/print", topic), fmt.Sprintf("?pool=%s", pool), "=count-only=") + if err != nil { + log.WithFields(log.Fields{ + "pool": pool, + "ip_version": ipVersion, + "device": ctx.device.Name, + "error": err, + }).Error("error fetching pool counts") + return err + } + + v, err := strconv.ParseFloat(reply.Done.Map["ret"], 32) + if err != nil { + log.WithFields(log.Fields{ + "pool": pool, + "ip_version": ipVersion, + "device": ctx.device.Name, + "error": err, + }).Error("error parsing pool counts") + return err + } + + ctx.ch <- prometheus.MustNewConstMetric(c.usedCountDesc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, ipVersion, pool) + return nil +} diff --git a/collector/resource_collector.go b/collector/resource_collector.go index 4ff353f..a90248a 100644 --- a/collector/resource_collector.go +++ b/collector/resource_collector.go @@ -4,53 +4,56 @@ import ( "strconv" "strings" - "github.com/nshttpd/mikrotik-exporter/config" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "gopkg.in/routeros.v2" "gopkg.in/routeros.v2/proto" ) -var ( - resourceLabelNames = []string{"name", "address"} - resourceProps = []string{"free-memory", "total-memory", "cpu-load", "free-hdd-space", "total-hdd-space"} - resourceDescriptions map[string]*prometheus.Desc -) +type resourceCollector struct { + props []string + descriptions map[string]*prometheus.Desc +} -func init() { - resourceDescriptions = make(map[string]*prometheus.Desc) - for _, p := range resourceProps { - resourceDescriptions[p] = descriptionForPropertyName("system", p, resourceLabelNames) +func newResourceCollector() routerOSCollector { + c := &resourceCollector{} + c.init() + return c +} + +func (c *resourceCollector) init() { + c.props = []string{"free-memory", "total-memory", "cpu-load", "free-hdd-space", "total-hdd-space"} + + labelNames := []string{"name", "address"} + c.descriptions = make(map[string]*prometheus.Desc) + for _, p := range c.props { + c.descriptions[p] = descriptionForPropertyName("system", p, labelNames) } } -type resourceCollector struct { -} - func (c *resourceCollector) describe(ch chan<- *prometheus.Desc) { - for _, d := range resourceDescriptions { + for _, d := range c.descriptions { ch <- d } } -func (c *resourceCollector) collect(ch chan<- prometheus.Metric, device *config.Device, client *routeros.Client) error { - stats, err := c.fetch(client, device) +func (c *resourceCollector) collect(ctx *collectorContext) error { + stats, err := c.fetch(ctx) if err != nil { return err } for _, re := range stats { - c.collectForStat(re, device, ch) + c.collectForStat(re, ctx) } return nil } -func (c *resourceCollector) fetch(client *routeros.Client, device *config.Device) ([]*proto.Sentence, error) { - reply, err := client.Run("/system/resource/print", "=.proplist="+strings.Join(resourceProps, ",")) +func (c *resourceCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) { + reply, err := ctx.client.Run("/system/resource/print", "=.proplist="+strings.Join(c.props, ",")) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error fetching system resource metrics") return nil, err @@ -59,17 +62,17 @@ func (c *resourceCollector) fetch(client *routeros.Client, device *config.Device return reply.Re, nil } -func (c *resourceCollector) collectForStat(re *proto.Sentence, device *config.Device, ch chan<- prometheus.Metric) { - for _, p := range resourceProps { - c.collectMetricForProperty(p, device, re, ch) +func (c *resourceCollector) collectForStat(re *proto.Sentence, ctx *collectorContext) { + for _, p := range c.props { + c.collectMetricForProperty(p, re, ctx) } } -func (c *resourceCollector) collectMetricForProperty(property string, device *config.Device, re *proto.Sentence, ch chan<- prometheus.Metric) { +func (c *resourceCollector) collectMetricForProperty(property string, re *proto.Sentence, ctx *collectorContext) { v, err := strconv.ParseFloat(re.Map[property], 64) if err != nil { log.WithFields(log.Fields{ - "device": device.Name, + "device": ctx.device.Name, "property": property, "value": re.Map[property], "error": err, @@ -77,6 +80,6 @@ func (c *resourceCollector) collectMetricForProperty(property string, device *co return } - desc := resourceDescriptions[property] - ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, device.Name, device.Address) + desc := c.descriptions[property] + ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, ctx.device.Name, ctx.device.Address) } diff --git a/collector/routeros_collector.go b/collector/routeros_collector.go new file mode 100644 index 0000000..bb4d6bf --- /dev/null +++ b/collector/routeros_collector.go @@ -0,0 +1,10 @@ +package collector + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type routerOSCollector interface { + describe(ch chan<- *prometheus.Desc) + collect(ctx *collectorContext) error +} diff --git a/collector/routes_collector.go b/collector/routes_collector.go index e2db62b..7b6b31c 100644 --- a/collector/routes_collector.go +++ b/collector/routes_collector.go @@ -4,95 +4,101 @@ import ( "fmt" "strconv" - "github.com/nshttpd/mikrotik-exporter/config" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "gopkg.in/routeros.v2" ) -const routesPrefiix = "routes" - -var ( - routesProtocols = []string{"bgp", "static", "ospf", "dynamic", "connect"} -) - -var ( - routesTotalDesc *prometheus.Desc - routesProtocolDesc *prometheus.Desc -) - -func init() { - l := []string{"name", "address", "ip_version"} - routesTotalDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, routesPrefiix, "total_count"), - "number of routes in RIB", - l, - nil, - ) - routesProtocolDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, routesPrefiix, "protocol_count"), - "number of routes per protocol in RIB", - append(l, "protocol"), - nil, - ) -} - type routesCollector struct { + protocols []string + countDesc *prometheus.Desc + countProtocolDesc *prometheus.Desc +} + +func newRoutesCollector() routerOSCollector { + c := &routesCollector{} + c.init() + return c +} + +func (c *routesCollector) init() { + const prefix = "routes" + labelNames := []string{"name", "address", "ip_version"} + c.countDesc = description(prefix, "total_count", "number of routes in RIB", labelNames) + c.countProtocolDesc = description(prefix, "protocol_count", "number of routes per protocol in RIB", append(labelNames, "protocol")) + + c.protocols = []string{"bgp", "static", "ospf", "dynamic", "connect"} } func (c *routesCollector) describe(ch chan<- *prometheus.Desc) { - ch <- routesTotalDesc - ch <- routesProtocolDesc + ch <- c.countDesc + ch <- c.countProtocolDesc } -func (c *routesCollector) collect(ch chan<- prometheus.Metric, device *config.Device, client *routeros.Client) error { - c.colllectForIPVersion(client, device, ch, "4", "ip") - c.colllectForIPVersion(client, device, ch, "6", "ipv6") +func (c *routesCollector) collect(ctx *collectorContext) error { + err := c.colllectForIPVersion("4", "ip", ctx) + if err != nil { + return err + } + + err = c.colllectForIPVersion("6", "ipv6", ctx) + if err != nil { + return err + } + return nil } -func (c *routesCollector) colllectForIPVersion(client *routeros.Client, device *config.Device, ch chan<- prometheus.Metric, ipVersion, topic string) { - c.colllectCount(client, device, ch, ipVersion, topic) - - for _, p := range routesProtocols { - c.colllectCountProtcol(client, device, ch, ipVersion, topic, p) +func (c *routesCollector) colllectForIPVersion(ipVersion, topic string, ctx *collectorContext) error { + err := c.colllectCount(ipVersion, topic, ctx) + if err != nil { + return err } + + for _, p := range c.protocols { + err := c.colllectCountProtcol(ipVersion, topic, p, ctx) + if err != nil { + return err + } + } + + return nil } -func (c *routesCollector) colllectCount(client *routeros.Client, device *config.Device, ch chan<- prometheus.Metric, ipVersion, topic string) { - reply, err := client.Run(fmt.Sprintf("/%s/route/print", topic), "?disabled=false", "=count-only=") +func (c *routesCollector) colllectCount(ipVersion, topic string, ctx *collectorContext) error { + reply, err := ctx.client.Run(fmt.Sprintf("/%s/route/print", topic), "?disabled=false", "=count-only=") if err != nil { log.WithFields(log.Fields{ "ip_version": ipVersion, - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error fetching routes metrics") - return + return err } v, err := strconv.ParseFloat(reply.Done.Map["ret"], 32) if err != nil { log.WithFields(log.Fields{ "ip_version": ipVersion, - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error parsing routes metrics") - return + return err } - ch <- prometheus.MustNewConstMetric(routesTotalDesc, prometheus.GaugeValue, v, device.Name, device.Address, ipVersion) + ctx.ch <- prometheus.MustNewConstMetric(c.countDesc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, ipVersion) + return nil } -func (c *routesCollector) colllectCountProtcol(client *routeros.Client, device *config.Device, ch chan<- prometheus.Metric, ipVersion, topic, protocol string) { - reply, err := client.Run(fmt.Sprintf("/%s/route/print", topic), "?disabled=false", fmt.Sprintf("?%s", protocol), "=count-only=") +func (c *routesCollector) colllectCountProtcol(ipVersion, topic, protocol string, ctx *collectorContext) error { + reply, err := ctx.client.Run(fmt.Sprintf("/%s/route/print", topic), "?disabled=false", fmt.Sprintf("?%s", protocol), "=count-only=") if err != nil { log.WithFields(log.Fields{ "ip_version": ipVersion, "protocol": protocol, - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error fetching routes metrics") - return + return err } v, err := strconv.ParseFloat(reply.Done.Map["ret"], 32) @@ -100,11 +106,12 @@ func (c *routesCollector) colllectCountProtcol(client *routeros.Client, device * log.WithFields(log.Fields{ "ip_version": ipVersion, "protocol": protocol, - "device": device.Name, + "device": ctx.device.Name, "error": err, }).Error("error parsing routes metrics") - return + return err } - ch <- prometheus.MustNewConstMetric(routesProtocolDesc, prometheus.GaugeValue, v, device.Name, device.Address, ipVersion, protocol) + ctx.ch <- prometheus.MustNewConstMetric(c.countProtocolDesc, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, ipVersion, protocol) + return nil } diff --git a/config/config.go b/config/config.go index b2f74df..d29c25c 100644 --- a/config/config.go +++ b/config/config.go @@ -7,10 +7,19 @@ import ( yaml "gopkg.in/yaml.v2" ) +// Config represents the configuration for the exporter type Config struct { - Devices []Device `yaml:"devices"` + 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"` + } `yaml:"features,omitempty"` } +// Device represents a target device type Device struct { Name string `yaml:"name"` Address string `yaml:"address"` @@ -18,6 +27,7 @@ type Device struct { 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 { diff --git a/config/config_test.go b/config/config_test.go index 637f17f..21d982b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,8 +17,13 @@ func TestShouldParse(t *testing.T) { t.Fatalf("expected 2 devices, got %v", len(c.Devices)) } - assertConfig("test1", "192.168.1.1", "foo", "bar", c.Devices[0], t) - assertConfig("test2", "192.168.2.1", "test", "123", c.Devices[1], t) + 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) } func loadTestFile(t *testing.T) []byte { @@ -30,7 +35,7 @@ func loadTestFile(t *testing.T) []byte { return b } -func assertConfig(name, address, user, password string, c Device, t *testing.T) { +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) } @@ -47,3 +52,9 @@ func assertConfig(name, address, user, password string, c Device, t *testing.T) 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 87e16e0..1b563d7 100644 --- a/config/test/config.test.yml +++ b/config/test/config.test.yml @@ -8,4 +8,11 @@ devices: - name: test2 address: 192.168.2.1 user: test - password: 123 \ No newline at end of file + password: 123 + +features: + bgp: true + dhcp: true + dhcpv6: true + routes: true + pools: true \ No newline at end of file diff --git a/main.go b/main.go index 5abcfcb..8ba1c85 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,9 @@ var ( configFile = flag.String("config-file", "", "config file to load") withBgp = flag.Bool("with-bgp", false, "retrieves BGP routing infrormation") withRoutes = flag.Bool("with-routes", false, "retrieves routing table information") + withDHCP = flag.Bool("with-dhcp", false, "retrieves DHCP server metrics") + withDHCPv6 = flag.Bool("with-dhcpv6", false, "retrieves DHCPv6 server metrics") + withPools = flag.Bool("with-pools", false, "retrieves IP(v6) pool 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)") @@ -100,7 +103,11 @@ func loadConfigFromFlags() (*config.Config, error) { } func startServer() { - http.HandleFunc(*metricsPath, prometheus.InstrumentHandlerFunc("prometheus", handler)) + h, err := createMetricsHandler() + if err != nil { + log.Fatal(err) + } + http.Handle(*metricsPath, h) http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) @@ -120,45 +127,49 @@ func startServer() { log.Fatal(http.ListenAndServe(*port, nil)) } -func handler(w http.ResponseWriter, r *http.Request) { +func createMetricsHandler() (http.Handler, error) { opts := collectorOptions() nc, err := collector.NewCollector(cfg, opts...) if err != nil { - log.Warnln("Couldn't create", err) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("Couldn't create %s", err))) - return + return nil, err } registry := prometheus.NewRegistry() err = registry.Register(nc) if err != nil { - log.Errorln("Couldn't register collector:", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Couldn't register collector: %s", err))) - return + return nil, err } - // Delegate http serving to Prometheus client library, which will call collector.Collect. - h := promhttp.HandlerFor(registry, + return promhttp.HandlerFor(registry, promhttp.HandlerOpts{ ErrorLog: log.New(), ErrorHandling: promhttp.ContinueOnError, - }) - h.ServeHTTP(w, r) + }), nil } func collectorOptions() []collector.Option { opts := []collector.Option{} - if *withBgp { + if *withBgp || cfg.Features.BGP { opts = append(opts, collector.WithBGP()) } - if *withRoutes { + if *withRoutes || cfg.Features.Routes { opts = append(opts, collector.WithRoutes()) } + if *withDHCP || cfg.Features.DHCP { + opts = append(opts, collector.WithDHCP()) + } + + if *withDHCPv6 || cfg.Features.DHCPv6 { + opts = append(opts, collector.WithDHCPv6()) + } + + if *withPools || cfg.Features.Pools { + opts = append(opts, collector.WithPools()) + } + if *timeout != collector.DefaultTimeout { opts = append(opts, collector.WithTimeout(*timeout)) }