From 2ea8ca84476a8302ad6f51acd33b2fcf0fb2ede7 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Tue, 28 Nov 2017 23:58:39 -0500 Subject: [PATCH] re-worked to actually properly work at collecting metrics --- collector/collector.go | 77 ++++++++++++++++++++ collector/config.go | 14 ++-- collector/device.go | 151 +++++++++++++++++++++++++--------------- collector/prometheus.go | 26 ++----- main.go | 85 +++++++++++++++------- 5 files changed, 244 insertions(+), 109 deletions(-) diff --git a/collector/collector.go b/collector/collector.go index 66f94de..99fd217 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -1 +1,78 @@ package collector + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" +) + +const namespace = "mikrotik" + +var ( + scrapeDurationDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "scrape", "collector_duration_seconds"), + "mikrotik_exporter: duration of a collector scrape", + []string{"device"}, + nil, + ) + scrapeSuccessDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "scrape", "collector_success"), + "mikrotik_exporter: whether a collector succeeded", + []string{"device"}, + nil, + ) +) + +type deviceCollector struct { + Devices []Device +} + +func NewDeviceCollector(cfg Config) (*deviceCollector, error) { + devices := make([]Device, len(cfg.Devices)) + + cfg.Logger.Info("setting up collector for devices", + "numDevices", len(cfg.Devices), + ) + + copy(devices, cfg.Devices) + + return &deviceCollector{Devices: devices}, nil +} + +// Describe implements the prometheus.Collector interface. +func (d deviceCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- scrapeDurationDesc + ch <- scrapeSuccessDesc +} + +// Collect implements the prometheus.Collector interface. +func (d deviceCollector) Collect(ch chan<- prometheus.Metric) { + wg := sync.WaitGroup{} + wg.Add(len(d.Devices)) + for _, device := range d.Devices { + go func(d Device) { + execute(d, ch) + wg.Done() + }(device) + } + wg.Wait() +} + +func execute(d Device, ch chan<- prometheus.Metric) { + begin := time.Now() + err := d.Update(ch) + duration := time.Since(begin) + var success float64 + + if err != nil { + log.Errorf("ERROR: %s collector failed after %fs: %s", d.name, duration.Seconds(), err) + success = 0 + } else { + log.Debugf("OK: %s collector succeeded after %fs.", d.name, duration.Seconds()) + success = 1 + } + ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), d.name) + ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, d.name) +} diff --git a/collector/config.go b/collector/config.go index 1d7d43b..7f8a014 100644 --- a/collector/config.go +++ b/collector/config.go @@ -1,7 +1,9 @@ -package exporter +package collector import ( "fmt" + + "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) @@ -17,10 +19,12 @@ func (c *Config) FromFlags(device, address, user, password *string) error { } d := &Device{ - Address: *address, - Name: *device, - User: *user, - Password: *password, + address: *address, + name: *device, + user: *user, + password: *password, + iDesc: map[string]*prometheus.Desc{}, + rDesc: map[string]*prometheus.Desc{}, } *c = Config{ diff --git a/collector/device.go b/collector/device.go index 86e253b..a51884c 100644 --- a/collector/device.go +++ b/collector/device.go @@ -1,81 +1,118 @@ -package exporter +package collector import ( - "go.uber.org/zap" - "gopkg.in/routeros.v2" - "strconv" "strings" + + "fmt" + + "strconv" + + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/routeros.v2" + "gopkg.in/routeros.v2/proto" ) const ( apiPort = ":8728" ) +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"} + resourceLabelNames = []string{"name", "address"} + ResourceProps = []string{"free-memory", "total-memory", "cpu-load", "free-hdd-space", "total-hdd-space"} +) + type Device struct { - Address string - Name string - User string - Password string + address string + name string + user string + password string + iDesc map[string]*prometheus.Desc // interface level descriptions for device + rDesc map[string]*prometheus.Desc // resource level descriptions for device } -func (d *Device) fetchInterfaceMetrics(c *routeros.Client, m PromMetrics, l *zap.SugaredLogger) error { - l.Debugw("fetching interface metrics", - "device", d.Name, - ) +func metricStringCleanup(in string) string { + return strings.Replace(in, "-", "_", -1) +} + +func (d *Device) fetchInterfaceMetrics() ([]*proto.Sentence, error) { + // clean up logging later TODO(smb) + //l.Debugw("fetching interface metrics", + // "device", d.name, + //) + + // grab a connection to the device + c, err := routeros.Dial(d.address+apiPort, d.user, d.password) + if err != nil { + // clean up logging later TODO(smb) + //l.Errorw("error dialing device", + // "device", d.name, + // "error", err, + //) + return nil, err + } + defer c.Close() reply, err := c.Run("/interface/print", "?disabled=false", "?running=true", "=.proplist="+strings.Join(InterfaceProps, ",")) if err != nil { - return err + // do some logging here about an error when we redo all the logging TODO(smb) + return nil, err } - for _, re := range reply.Re { - var name string - // name should always be first element on the array - for _, p := range InterfaceProps { - if p == "name" { - name = re.Map[p] - } else { - v, err := strconv.ParseFloat(re.Map[p], 64) - if err != nil { - l.Errorw("error parsing value to float", - "device", d.Name, - "property", p, - "value", re.Map[p], - "error", err, - ) + return reply.Re, nil + + //for _, re := range reply.Re { + // var name string + // // name should always be first element on the array + // for _, p := range InterfaceProps { + // if p == "name" { + // name = re.Map[p] + // } else { + // v, err := strconv.ParseFloat(re.Map[p], 64) + // if err != nil { + // l.Errorw("error parsing value to float", + // "device", d.name, + // "property", p, + // "value", re.Map[p], + // "error", err, + // ) + // } + // m.IncrementInterface(p, d.name, d.address, name, v) + // } + // } + //} +} + +func (d *Device) Update(ch chan<- prometheus.Metric) error { + + stats, err := d.fetchInterfaceMetrics() + // if there is no error, deal with the response + if err == nil { + for _, re := range stats { + var intf string + for _, p := range InterfaceProps { + if p == "name" { + intf = re.Map[p] + } else { + desc, ok := d.iDesc[p] + if !ok { + desc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "interface", metricStringCleanup(p)), + fmt.Sprintf("interface property statistic %s", p), + interfaceLabelNames, + nil, + ) + d.iDesc[p] = desc + } + v, err := strconv.ParseFloat(re.Map[p], 64) + if err == nil { + ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, d.name, d.address, intf) + } // add an else with logging here when logging is re done TODO(smb) } - m.IncrementInterface(p, d.Name, d.Address, name, v) } } } - - l.Debugw("done fetching interface metrics", - "device", d.Name, - ) - - return nil -} - -func (d *Device) CollectMetrics(p PromMetrics, l *zap.SugaredLogger) error { - - c, err := routeros.Dial(d.Address+apiPort, d.User, d.Password) - if err != nil { - l.Errorw("error dialing device", - "device", d.Name, - "error", err, - ) - return err - } - defer c.Close() - - if err := d.fetchInterfaceMetrics(c, p, l); err != nil { - l.Errorw("error fetching interface metrics", - "device", d.Name, - "error", err, - ) - return err - } - return nil } diff --git a/collector/prometheus.go b/collector/prometheus.go index 90298f5..a39408c 100644 --- a/collector/prometheus.go +++ b/collector/prometheus.go @@ -1,24 +1,12 @@ -package exporter +package collector import ( - "strings" - "fmt" + "net/http" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" - "net/http" -) - -const ( - promNamespace = "mikrotik" -) - -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"} - resourceLabelNames = []string{"name", "address"} - ResourceProps = []string{"free-memory", "total-memory", "cpu-load", "free-hdd-space", "total-hdd-space"} ) type PromMetrics struct { @@ -26,10 +14,6 @@ type PromMetrics struct { ResourceMetrics map[string]*prometheus.GaugeVec } -func metricStringCleanup(in string) string { - return strings.Replace(in, "-", "_", -1) -} - func (p *PromMetrics) makeLabels(name, address string) prometheus.Labels { labels := make(prometheus.Labels) labels["name"] = metricStringCleanup(name) @@ -51,7 +35,7 @@ func (p *PromMetrics) SetupPrometheus(l zap.SugaredLogger) (http.Handler, error) for _, v := range InterfaceProps { n := metricStringCleanup(v) c := prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: promNamespace, + Namespace: namespace, Subsystem: "interface", Name: n, Help: fmt.Sprintf("Interface %s counter", v), @@ -72,7 +56,7 @@ func (p *PromMetrics) SetupPrometheus(l zap.SugaredLogger) (http.Handler, error) for _, v := range ResourceProps { n := metricStringCleanup(v) c := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: promNamespace, + Namespace: namespace, Subsystem: "resource", Name: n, Help: fmt.Sprintf("Resource %s counter", v), diff --git a/main.go b/main.go index 7bb992d..d816cb5 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,15 @@ package main import ( "flag" "os" - "os/signal" - "github.com/nshttpd/mikrotik-exporter/exporter" + "fmt" + "net/http" + + "github.com/nshttpd/mikrotik-exporter/collector" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/log" + "github.com/prometheus/common/version" "go.uber.org/zap" ) @@ -19,10 +24,45 @@ var ( cfgFile = flag.String("config", "", "config file for multiple devices") logLevel = flag.String("log-level", "info", "log level") port = flag.String("port", ":9090", "port number to listen on") + metricsPath = flag.String("path", "/metrics", "path to answer requests on") currentLogLevel = zap.NewAtomicLevelAt(zap.InfoLevel) + cfg collector.Config ) -// (nshttpd) TODO figure out if we need a caching option +func init() { + prometheus.MustRegister(version.NewCollector("mikrotik_exporter")) +} + +func handler(w http.ResponseWriter, r *http.Request) { + nc, err := collector.NewDeviceCollector(cfg) + if err != nil { + log.Warnln("Couldn't create", err) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Couldn't create %s", err))) + return + } + + 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 + } + + gatherers := prometheus.Gatherers{ + prometheus.DefaultGatherer, + registry, + } + // Delegate http serving to Prometheus client library, which will call collector.Collect. + h := promhttp.HandlerFor(gatherers, + promhttp.HandlerOpts{ + ErrorLog: log.NewErrorLogger(), + ErrorHandling: promhttp.ContinueOnError, + }) + h.ServeHTTP(w, r) +} func main() { flag.Parse() @@ -42,7 +82,6 @@ func main() { } defer l.Sync() - var cfg exporter.Config if *cfgFile == "" { if err := cfg.FromFlags(device, address, user, password); err != nil { l.Sugar().Errorw("could not create configuration", @@ -57,32 +96,26 @@ func main() { cfg.Logger = l.Sugar() - cfg.Metrics = exporter.PromMetrics{} - mh, err := cfg.Metrics.SetupPrometheus(*cfg.Logger) + http.HandleFunc(*metricsPath, prometheus.InstrumentHandlerFunc("prometheus", handler)) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + Mikrotik Exporter + +

Mikrotik Exporter

+

Metrics

+ + `)) + }) + + log.Infoln("Listening on", *port) + err = http.ListenAndServe(*port, nil) if err != nil { log.Fatal(err) } - s := &exporter.Server{} - - if err := s.Run(cfg, mh, port); err != nil { - log.Fatal(err) - } - - sigchan := make(chan os.Signal, 1) - signal.Notify(sigchan, os.Interrupt, os.Kill) - <-sigchan - cfg.Logger.Info("stopping server") - err = s.Stop() - if err != nil { - cfg.Logger.Errorw("error while stopping service", - "error", err, - ) - os.Exit(1) - } - - os.Exit(0) - } func newLogger(lvl zap.AtomicLevel) (*zap.Logger, error) {