Merge pull request #2 from nshttpd/nshttpd/counter-fix

redo to actually work as it's supposed to.
This commit is contained in:
Steve Brunton 2017-11-29 08:15:11 -05:00 committed by GitHub
commit 13b50a6b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 271 additions and 229 deletions

1
MAINTAINERS.md Normal file
View File

@ -0,0 +1 @@
* Steve Brunton <sbrunton@gmail.com> @nshttpd

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.0.0

78
collector/collector.go Normal file
View 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)
}

View File

@ -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
View 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
}

View File

@ -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),

View File

@ -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
}

View File

@ -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
View File

@ -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) {

View File

@ -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

View File

@ -1 +0,0 @@
command-line-arguments.test

View File

@ -1,5 +0,0 @@
.vscode
.idea
*.exe
*.out
*.test