re-worked to actually properly work at collecting metrics

This commit is contained in:
Steve Brunton 2017-11-28 23:58:39 -05:00
parent 3576779a56
commit 2ea8ca8447
5 changed files with 244 additions and 109 deletions

View File

@ -1 +1,78 @@
package collector 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)
}

View File

@ -1,7 +1,9 @@
package exporter package collector
import ( import (
"fmt" "fmt"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -17,10 +19,12 @@ func (c *Config) FromFlags(device, address, user, password *string) error {
} }
d := &Device{ d := &Device{
Address: *address, address: *address,
Name: *device, name: *device,
User: *user, user: *user,
Password: *password, password: *password,
iDesc: map[string]*prometheus.Desc{},
rDesc: map[string]*prometheus.Desc{},
} }
*c = Config{ *c = Config{

View File

@ -1,81 +1,118 @@
package exporter package collector
import ( import (
"go.uber.org/zap"
"gopkg.in/routeros.v2"
"strconv"
"strings" "strings"
"fmt"
"strconv"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/routeros.v2"
"gopkg.in/routeros.v2/proto"
) )
const ( const (
apiPort = ":8728" 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 { type Device struct {
Address string address string
Name string name string
User string user string
Password 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 { func metricStringCleanup(in string) string {
l.Debugw("fetching interface metrics", return strings.Replace(in, "-", "_", -1)
"device", d.Name, }
)
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", reply, err := c.Run("/interface/print", "?disabled=false",
"?running=true", "=.proplist="+strings.Join(InterfaceProps, ",")) "?running=true", "=.proplist="+strings.Join(InterfaceProps, ","))
if err != nil { 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 { return reply.Re, nil
var name string
// name should always be first element on the array //for _, re := range reply.Re {
for _, p := range InterfaceProps { // var name string
if p == "name" { // // name should always be first element on the array
name = re.Map[p] // for _, p := range InterfaceProps {
} else { // if p == "name" {
v, err := strconv.ParseFloat(re.Map[p], 64) // name = re.Map[p]
if err != nil { // } else {
l.Errorw("error parsing value to float", // v, err := strconv.ParseFloat(re.Map[p], 64)
"device", d.Name, // if err != nil {
"property", p, // l.Errorw("error parsing value to float",
"value", re.Map[p], // "device", d.name,
"error", err, // "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 return nil
} }

View File

@ -1,24 +1,12 @@
package exporter package collector
import ( import (
"strings"
"fmt" "fmt"
"net/http"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap" "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 { type PromMetrics struct {
@ -26,10 +14,6 @@ type PromMetrics struct {
ResourceMetrics map[string]*prometheus.GaugeVec ResourceMetrics map[string]*prometheus.GaugeVec
} }
func metricStringCleanup(in string) string {
return strings.Replace(in, "-", "_", -1)
}
func (p *PromMetrics) makeLabels(name, address string) prometheus.Labels { func (p *PromMetrics) makeLabels(name, address string) prometheus.Labels {
labels := make(prometheus.Labels) labels := make(prometheus.Labels)
labels["name"] = metricStringCleanup(name) labels["name"] = metricStringCleanup(name)
@ -51,7 +35,7 @@ func (p *PromMetrics) SetupPrometheus(l zap.SugaredLogger) (http.Handler, error)
for _, v := range InterfaceProps { for _, v := range InterfaceProps {
n := metricStringCleanup(v) n := metricStringCleanup(v)
c := prometheus.NewCounterVec(prometheus.CounterOpts{ c := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: promNamespace, Namespace: namespace,
Subsystem: "interface", Subsystem: "interface",
Name: n, Name: n,
Help: fmt.Sprintf("Interface %s counter", v), 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 { for _, v := range ResourceProps {
n := metricStringCleanup(v) n := metricStringCleanup(v)
c := prometheus.NewGaugeVec(prometheus.GaugeOpts{ c := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: promNamespace, Namespace: namespace,
Subsystem: "resource", Subsystem: "resource",
Name: n, Name: n,
Help: fmt.Sprintf("Resource %s counter", v), Help: fmt.Sprintf("Resource %s counter", v),

85
main.go
View File

@ -3,10 +3,15 @@ package main
import ( import (
"flag" "flag"
"os" "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/log"
"github.com/prometheus/common/version"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -19,10 +24,45 @@ var (
cfgFile = flag.String("config", "", "config file for multiple devices") cfgFile = flag.String("config", "", "config file for multiple devices")
logLevel = flag.String("log-level", "info", "log level") logLevel = flag.String("log-level", "info", "log level")
port = flag.String("port", ":9090", "port number to listen on") 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) 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() { func main() {
flag.Parse() flag.Parse()
@ -42,7 +82,6 @@ func main() {
} }
defer l.Sync() defer l.Sync()
var cfg exporter.Config
if *cfgFile == "" { if *cfgFile == "" {
if err := cfg.FromFlags(device, address, user, password); err != nil { if err := cfg.FromFlags(device, address, user, password); err != nil {
l.Sugar().Errorw("could not create configuration", l.Sugar().Errorw("could not create configuration",
@ -57,32 +96,26 @@ func main() {
cfg.Logger = l.Sugar() cfg.Logger = l.Sugar()
cfg.Metrics = exporter.PromMetrics{} http.HandleFunc(*metricsPath, prometheus.InstrumentHandlerFunc("prometheus", handler))
mh, err := cfg.Metrics.SetupPrometheus(*cfg.Logger) 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 { if err != nil {
log.Fatal(err) 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) { func newLogger(lvl zap.AtomicLevel) (*zap.Logger, error) {