mirror of
https://github.com/nshttpd/mikrotik-exporter.git
synced 2024-11-16 10:25:12 +01:00
Merge pull request #2 from nshttpd/nshttpd/counter-fix
redo to actually work as it's supposed to.
This commit is contained in:
commit
13b50a6b7c
1
MAINTAINERS.md
Normal file
1
MAINTAINERS.md
Normal file
@ -0,0 +1 @@
|
||||
* Steve Brunton <sbrunton@gmail.com> @nshttpd
|
78
collector/collector.go
Normal file
78
collector/collector.go
Normal file
@ -0,0 +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)
|
||||
}
|
@ -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{
|
118
collector/device.go
Normal file
118
collector/device.go
Normal file
@ -0,0 +1,118 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"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
|
||||
iDesc map[string]*prometheus.Desc // interface level descriptions for device
|
||||
rDesc map[string]*prometheus.Desc // resource level descriptions for device
|
||||
}
|
||||
|
||||
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 {
|
||||
// do some logging here about an error when we redo all the logging TODO(smb)
|
||||
return nil, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -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),
|
@ -1,81 +0,0 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/routeros.v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
apiPort = ":8728"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Address string
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (d *Device) fetchInterfaceMetrics(c *routeros.Client, m PromMetrics, l *zap.SugaredLogger) error {
|
||||
l.Debugw("fetching interface metrics",
|
||||
"device", d.Name,
|
||||
)
|
||||
|
||||
reply, err := c.Run("/interface/print", "?disabled=false",
|
||||
"?running=true", "=.proplist="+strings.Join(InterfaceProps, ","))
|
||||
if err != nil {
|
||||
return 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,
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
@ -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()
|
||||
}
|
85
main.go
85
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(`<html>
|
||||
<head><title>Mikrotik Exporter</title></head>
|
||||
<body>
|
||||
<h1>Mikrotik Exporter</h1>
|
||||
<p><a href="` + *metricsPath + `">Metrics</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
})
|
||||
|
||||
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) {
|
||||
|
26
vendor/github.com/prometheus/client_golang/.gitignore
generated
vendored
26
vendor/github.com/prometheus/client_golang/.gitignore
generated
vendored
@ -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
|
1
vendor/github.com/prometheus/client_golang/prometheus/.gitignore
generated
vendored
1
vendor/github.com/prometheus/client_golang/prometheus/.gitignore
generated
vendored
@ -1 +0,0 @@
|
||||
command-line-arguments.test
|
5
vendor/gopkg.in/routeros.v2/.gitignore
generated
vendored
5
vendor/gopkg.in/routeros.v2/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
||||
.vscode
|
||||
.idea
|
||||
*.exe
|
||||
*.out
|
||||
*.test
|
Loading…
Reference in New Issue
Block a user