From a0251ca4e8d6848a988d9fe4f6cd8d1c55ba4107 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Tue, 3 Oct 2017 22:24:14 -0400 Subject: [PATCH 1/5] creating some more files --- MAINTAINERS.md | 0 VERSION | 0 collector/collector.go | 1 + 3 files changed, 1 insertion(+) create mode 100644 MAINTAINERS.md create mode 100644 VERSION create mode 100644 collector/collector.go diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..e69de29 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e69de29 diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..66f94de --- /dev/null +++ b/collector/collector.go @@ -0,0 +1 @@ +package collector From a47f1ca2fce30e403e94e41b31659da4ec9c09e9 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Thu, 12 Oct 2017 08:36:20 -0400 Subject: [PATCH 2/5] clean up and adding in additional files --- MAINTAINERS.md | 1 + VERSION | 1 + .../prometheus/client_golang/.gitignore | 26 ------------------- .../client_golang/prometheus/.gitignore | 1 - vendor/gopkg.in/routeros.v2/.gitignore | 5 ---- 5 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 vendor/github.com/prometheus/client_golang/.gitignore delete mode 100644 vendor/github.com/prometheus/client_golang/prometheus/.gitignore delete mode 100644 vendor/gopkg.in/routeros.v2/.gitignore diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e69de29..f9fbeeb 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -0,0 +1 @@ +* Steve Brunton @nshttpd \ No newline at end of file diff --git a/VERSION b/VERSION index e69de29..bd52db8 100644 --- a/VERSION +++ b/VERSION @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file diff --git a/vendor/github.com/prometheus/client_golang/.gitignore b/vendor/github.com/prometheus/client_golang/.gitignore deleted file mode 100644 index f6fc2e8..0000000 --- a/vendor/github.com/prometheus/client_golang/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe - -*~ -*# -.build diff --git a/vendor/github.com/prometheus/client_golang/prometheus/.gitignore b/vendor/github.com/prometheus/client_golang/prometheus/.gitignore deleted file mode 100644 index 3460f03..0000000 --- a/vendor/github.com/prometheus/client_golang/prometheus/.gitignore +++ /dev/null @@ -1 +0,0 @@ -command-line-arguments.test diff --git a/vendor/gopkg.in/routeros.v2/.gitignore b/vendor/gopkg.in/routeros.v2/.gitignore deleted file mode 100644 index 1529658..0000000 --- a/vendor/gopkg.in/routeros.v2/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.vscode -.idea -*.exe -*.out -*.test From e738cf67ec47c4d3dc44ac2c4311ee0a0a63e533 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Sun, 22 Oct 2017 16:33:05 -0400 Subject: [PATCH 3/5] refactor moving things around --- {exporter => collector}/config.go | 0 {exporter => collector}/device.go | 0 {exporter => collector}/prometheus.go | 0 exporter/server.go => server.go | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {exporter => collector}/config.go (100%) rename {exporter => collector}/device.go (100%) rename {exporter => collector}/prometheus.go (100%) rename exporter/server.go => server.go (100%) diff --git a/exporter/config.go b/collector/config.go similarity index 100% rename from exporter/config.go rename to collector/config.go diff --git a/exporter/device.go b/collector/device.go similarity index 100% rename from exporter/device.go rename to collector/device.go diff --git a/exporter/prometheus.go b/collector/prometheus.go similarity index 100% rename from exporter/prometheus.go rename to collector/prometheus.go diff --git a/exporter/server.go b/server.go similarity index 100% rename from exporter/server.go rename to server.go From 3576779a56bb3a508db08cd324687bc440bec965 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Tue, 28 Nov 2017 23:58:02 -0500 Subject: [PATCH 4/5] removed server.go --- server.go | 64 ------------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 server.go diff --git a/server.go b/server.go deleted file mode 100644 index 21bfe99..0000000 --- a/server.go +++ /dev/null @@ -1,64 +0,0 @@ -package exporter - -import ( - "net" - "net/http" - "time" -) - -type Server struct { - l net.Listener -} - -func runCollector(cfg Config) { - cfg.Logger.Info("starting collector") - - for { - for _, d := range cfg.Devices { - d.CollectMetrics(cfg.Metrics, cfg.Logger) - } - time.Sleep(15 * time.Second) - } - -} - -func (s *Server) Run(cfg Config, mh http.Handler, port *string) error { - - cfg.Logger.Infow("starting server", - "port", *port, - ) - - var err error - s.l, err = net.Listen("tcp", *port) - if err != nil { - cfg.Logger.Errorw("error creating listener", - "port", *port, - "error", err, - ) - return err - } - - go func() { - runCollector(cfg) - }() - - mux := http.NewServeMux() - mux.Handle("/metrics", mh) - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("ok")) - }) - - go func() { - if err := http.Serve(s.l, mux); err != nil { - cfg.Logger.Errorw("unable to start service", - "error", err, - ) - } - }() - - return nil -} - -func (s *Server) Stop() error { - return s.l.Close() -} From 2ea8ca84476a8302ad6f51acd33b2fcf0fb2ede7 Mon Sep 17 00:00:00 2001 From: Steve Brunton Date: Tue, 28 Nov 2017 23:58:39 -0500 Subject: [PATCH 5/5] 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) {