From 39e54be98d634965e57e27af3ecfb0657eeff66c Mon Sep 17 00:00:00 2001 From: Daniel Czerwonk Date: Mon, 7 May 2018 20:31:19 +0200 Subject: [PATCH] Added optical diagnostic metrics (#10) --- Dockerfile | 2 +- README.md | 1 + collector/collector.go | 7 ++ collector/interface_collector.go | 20 ++-- collector/optics_collector.go | 153 +++++++++++++++++++++++++++++++ config/config.go | 1 + config/config_test.go | 1 + config/test/config.test.yml | 3 +- main.go | 7 +- 9 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 collector/optics_collector.go diff --git a/Dockerfile b/Dockerfile index 7c24e40..6885b1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:3.6 -EXPOSE 9090 +EXPOSE 9436 COPY scripts/start.sh /app/ COPY mikrotik-exporter /app/ diff --git a/README.md b/README.md index 7215da6..da46ddf 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ features: dhcpv6: true routes: true pools: true + optics: true ``` ###### example output diff --git a/collector/collector.go b/collector/collector.go index d67e37a..aee6db5 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -78,6 +78,13 @@ func WithPools() Option { } } +// WithOptics enables optical diagnstocs +func WithOptics() Option { + return func(c *collector) { + c.collectors = append(c.collectors, newOpticsCollector()) + } +} + // WithTimeout sets timeout for connecting to router func WithTimeout(d time.Duration) Option { return func(c *collector) { diff --git a/collector/interface_collector.go b/collector/interface_collector.go index b40e824..23d8d6d 100644 --- a/collector/interface_collector.go +++ b/collector/interface_collector.go @@ -21,9 +21,9 @@ func newInterfaceCollector() routerOSCollector { } func (c *interfaceCollector) init() { - c.props = []string{"name", "rx-byte", "tx-byte", "rx-packet", "tx-packet", "rx-error", "tx-error", "rx-drop", "tx-drop"} + c.props = []string{"name", "comment", "rx-byte", "tx-byte", "rx-packet", "tx-packet", "rx-error", "tx-error", "rx-drop", "tx-drop"} - labelNames := []string{"name", "address", "interface"} + labelNames := []string{"name", "address", "interface", "comment"} c.descriptions = make(map[string]*prometheus.Desc) for _, p := range c.props[1:] { c.descriptions[p] = descriptionForPropertyName("interface", p, labelNames) @@ -63,17 +63,15 @@ func (c *interfaceCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, er } func (c *interfaceCollector) collectForStat(re *proto.Sentence, ctx *collectorContext) { - var iface string - for _, p := range c.props { - if p == "name" { - iface = re.Map[p] - } else { - c.collectMetricForProperty(p, iface, re, ctx) - } + name := re.Map["name"] + comment := re.Map["comment"] + + for _, p := range c.props[2:] { + c.collectMetricForProperty(p, name, comment, re, ctx) } } -func (c *interfaceCollector) collectMetricForProperty(property, iface string, re *proto.Sentence, ctx *collectorContext) { +func (c *interfaceCollector) collectMetricForProperty(property, iface, comment string, re *proto.Sentence, ctx *collectorContext) { desc := c.descriptions[property] v, err := strconv.ParseFloat(re.Map[property], 64) if err != nil { @@ -87,5 +85,5 @@ func (c *interfaceCollector) collectMetricForProperty(property, iface string, re return } - ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, ctx.device.Name, ctx.device.Address, iface) + ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, ctx.device.Name, ctx.device.Address, iface, comment) } diff --git a/collector/optics_collector.go b/collector/optics_collector.go new file mode 100644 index 0000000..46d12cf --- /dev/null +++ b/collector/optics_collector.go @@ -0,0 +1,153 @@ +package collector + +import ( + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/routeros.v2/proto" +) + +type opticsCollector struct { + rxStatusDesc *prometheus.Desc + txStatusDesc *prometheus.Desc + rxPowerDesc *prometheus.Desc + txPowerDesc *prometheus.Desc + temperatureDesc *prometheus.Desc + txBiasDesc *prometheus.Desc + voltageDesc *prometheus.Desc + props []string +} + +func newOpticsCollector() routerOSCollector { + const prefix = "optics" + + labelNames := []string{"name", "address", "interface"} + return &opticsCollector{ + rxStatusDesc: description(prefix, "rx_status", "RX status (1 = no loss)", labelNames), + txStatusDesc: description(prefix, "tx_status", "TX status (1 = no faults)", labelNames), + rxPowerDesc: description(prefix, "rx_power_dbm", "RX power in dBM", labelNames), + txPowerDesc: description(prefix, "tx_power_dbm", "TX power in dBM", labelNames), + temperatureDesc: description(prefix, "temperature_celsius", "temperature in degree celsius", labelNames), + txBiasDesc: description(prefix, "tx_bias_ma", "bias is milliamps", labelNames), + voltageDesc: description(prefix, "voltage_volt", "volage in volt", labelNames), + props: []string{"sfp-rx-loss", "sfp-tx-fault", "sfp-temperature", "sfp-supply-voltage", "sfp-tx-bias-current", "sfp-tx-power", "sfp-rx-power"}, + } +} + +func (c *opticsCollector) describe(ch chan<- *prometheus.Desc) { + ch <- c.rxStatusDesc + ch <- c.txStatusDesc + ch <- c.rxPowerDesc + ch <- c.txPowerDesc + ch <- c.temperatureDesc + ch <- c.txBiasDesc + ch <- c.voltageDesc +} + +func (c *opticsCollector) collect(ctx *collectorContext) error { + reply, err := ctx.client.Run("/interface/ethernet/print", "=.proplist=name") + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching interface metrics") + return err + } + + ifaces := make([]string, 0) + for _, iface := range reply.Re { + n := iface.Map["name"] + if strings.HasPrefix(n, "sfp") { + ifaces = append(ifaces, n) + } + } + + if len(ifaces) == 0 { + return nil + } + + return c.collectOpticalMetricsForInterfaces(ifaces, ctx) +} + +func (c *opticsCollector) collectOpticalMetricsForInterfaces(ifaces []string, ctx *collectorContext) error { + reply, err := ctx.client.Run("/interface/ethernet/monitor", + "=numbers="+strings.Join(ifaces, ","), + "=once=", + "=.proplist=name,"+strings.Join(c.props, ",")) + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "error": err, + }).Error("error fetching interface monitor metrics") + return err + } + + for _, se := range reply.Re { + name, ok := se.Map["name"] + if !ok { + continue + } + + c.collectMetricsForInterface(name, se, ctx) + } + + return nil +} + +func (c *opticsCollector) collectMetricsForInterface(name string, se *proto.Sentence, ctx *collectorContext) { + for _, prop := range c.props { + v, ok := se.Map[prop] + if !ok { + continue + } + + value, err := c.valueForKey(prop, v) + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "interface": name, + "property": prop, + "error": err, + }).Error("error parsing interface monitor metric") + return + } + + ctx.ch <- prometheus.MustNewConstMetric(c.descForKey(prop), prometheus.GaugeValue, value, ctx.device.Name, ctx.device.Address, name) + } +} + +func (c *opticsCollector) valueForKey(name, value string) (float64, error) { + if name == "sfp-rx-loss" || name == "sfp-tx-fault" { + status := float64(1) + if value == "true" { + status = float64(0) + } + + return status, nil + } + + return strconv.ParseFloat(value, 64) +} + +func (c *opticsCollector) descForKey(name string) *prometheus.Desc { + switch name { + case "sfp-rx-loss": + return c.rxStatusDesc + case "sfp-tx-fault": + return c.txStatusDesc + case "sfp-temperature": + return c.temperatureDesc + case "sfp-supply-voltage": + return c.voltageDesc + case "sfp-tx-bias-current": + return c.txBiasDesc + case "sfp-tx-power": + return c.txPowerDesc + case "sfp-rx-power": + return c.rxPowerDesc + } + + return nil +} diff --git a/config/config.go b/config/config.go index d29c25c..b7ec9fe 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ type Config struct { DHCPv6 bool `yaml:"dhcpv6,omitempty"` Routes bool `yaml:"routes,omitempty"` Pools bool `yaml:"pools,omitempty"` + Optics bool `yaml:"optics,omitempty"` } `yaml:"features,omitempty"` } diff --git a/config/config_test.go b/config/config_test.go index 21d982b..7aaf1d2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -24,6 +24,7 @@ func TestShouldParse(t *testing.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 { diff --git a/config/test/config.test.yml b/config/test/config.test.yml index 1b563d7..b148b28 100644 --- a/config/test/config.test.yml +++ b/config/test/config.test.yml @@ -15,4 +15,5 @@ features: dhcp: true dhcpv6: true routes: true - pools: true \ No newline at end of file + pools: true + optics: true \ No newline at end of file diff --git a/main.go b/main.go index 8ba1c85..36eae7d 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ var ( password = flag.String("password", "", "password for authentication for single device") logLevel = flag.String("log-level", "info", "log level") logFormat = flag.String("log-format", "json", "logformat text or json (default json)") - port = flag.String("port", ":9090", "port number to listen on") + port = flag.String("port", ":9436", "port number to listen on") metricsPath = flag.String("path", "/metrics", "path to answer requests on") configFile = flag.String("config-file", "", "config file to load") withBgp = flag.Bool("with-bgp", false, "retrieves BGP routing infrormation") @@ -34,6 +34,7 @@ var ( 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") + withOptics = flag.Bool("with-optics", false, "retrieves optical diagnostic 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)") @@ -170,6 +171,10 @@ func collectorOptions() []collector.Option { opts = append(opts, collector.WithPools()) } + if *withOptics || cfg.Features.Optics { + opts = append(opts, collector.WithOptics()) + } + if *timeout != collector.DefaultTimeout { opts = append(opts, collector.WithTimeout(*timeout)) }