Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] added a new cloud service collector #157

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions collector/cloud_collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package collector

import (
"fmt"
"strings"

"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"gopkg.in/routeros.v2/proto"
)

type cloudCollector struct {
props []string
descriptions map[string]*prometheus.Desc
}

func newCloudCollector() routerOSCollector {
c := &cloudCollector{}
c.init()
return c
}

func (c *cloudCollector) init() {
c.props = []string{"public-address", "ddns-enabled"}
labelNames := []string{"name", "address", "public_address"}
c.descriptions = make(map[string]*prometheus.Desc)
for _, p := range c.props[1:] {
c.descriptions[p] = descriptionForPropertyName("cloud", p, labelNames)
}
}

func (c *cloudCollector) describe(ch chan<- *prometheus.Desc) {
for _, d := range c.descriptions {
ch <- d
}
}

func (c *cloudCollector) collect(ctx *collectorContext) error {
stats, err := c.fetch(ctx)
if err != nil {
return err
}

for _, re := range stats {
c.collectForStat(re, ctx)
}

return nil
}

func (c *cloudCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) {
reply, err := ctx.client.Run("/ip/cloud/print", "=.proplist="+strings.Join(c.props, ","))
if err != nil {
log.WithFields(log.Fields{
"device": ctx.device.Name,
"error": err,
}).Error("error fetching cloud metrics")
return nil, err
}

return reply.Re, nil
}

func (c *cloudCollector) collectForStat(re *proto.Sentence, ctx *collectorContext) {
publicIp := re.Map["public-address"]

for _, p := range c.props[1:] {
c.collectMetricForProperty(p, publicIp, re, ctx)
}
}

func (c *cloudCollector) collectMetricForProperty(property, publicIp string, re *proto.Sentence, ctx *collectorContext) {
desc := c.descriptions[property]
if value := re.Map[property]; value != "" {
var numericValue float64
switch value {
case "false":
numericValue = 0
case "true":
numericValue = 1
default:
log.WithFields(log.Fields{
"device": ctx.device.Name,
"public-address": publicIp,
"property": property,
"value": value,
"error": fmt.Errorf("unexpected cloud ddns-enabled value"),
}).Error("error parsing cloud metric value")
}

ctx.ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, numericValue, ctx.device.Name, ctx.device.Address, publicIp)
}
}
163 changes: 163 additions & 0 deletions collector/cloud_collector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package collector

import (
"io"
"mikrotik-exporter/config"
"testing"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
routeros "gopkg.in/routeros.v2"
"gopkg.in/routeros.v2/proto"
)

func TestCloudMetricDesc(t *testing.T) {
c := getFakeClient(t, "false")
defer c.Close()

cloudCollector := newCloudCollector()
metrics := make(chan prometheus.Metric, 1)

ctx := collectorContext{
ch: metrics,
device: &config.Device{Name: "foo", Address: "test"},
client: c,
}

cloudCollector.collect(&ctx)
m := <-metrics

d := descriptionForPropertyName("cloud", "ddns-enabled", []string{"name", "address", "public_address"})

assert.Equal(t, d, m.Desc(), "metrics description missmatch")
}

func TestCloudCollectFalse(t *testing.T) {
var pb dto.Metric

c := getFakeClient(t, "false")
defer c.Close()

cloudCollector := newCloudCollector()
metrics := make(chan prometheus.Metric, 1)

ctx := collectorContext{
ch: metrics,
device: &config.Device{Name: "foo", Address: "test"},
client: c,
}

cloudCollector.collect(&ctx)
m := <-metrics

assert.NoError(t, nil, m.Write(&pb), "error reading metrics")
assert.Equal(t, float64(0), pb.Counter.GetValue(), "excpeted output should be 0 for false")

for _, l := range pb.Label {
switch l.GetName() {
case "name":
assert.Equal(t, "foo", l.GetValue(), "device name label value missmatch")
case "address":
assert.Equal(t, "test", l.GetValue(), "device address label value missmatch")
case "public_address":
assert.Equal(t, "0.0.0.0", l.GetValue(), "public_address label value missmatch")
default:
t.Fatalf("invalid or missing lables %s", l.GetName())
}
}
}

func TestCloudCollectTrue(t *testing.T) {
var pb dto.Metric

c := getFakeClient(t, "true")
defer c.Close()

cloudCollector := newCloudCollector()
metrics := make(chan prometheus.Metric, 1)

ctx := collectorContext{
ch: metrics,
device: &config.Device{Name: "foo", Address: "test"},
client: c,
}

cloudCollector.collect(&ctx)
m := <-metrics

assert.NoError(t, nil, m.Write(&pb))
assert.Equal(t, float64(1), pb.Counter.GetValue(), "excpeted output should be 1 for true")
}

func getFakeClient(t *testing.T, state string) *routeros.Client {
c, s := newPair(t)

go func() {
defer s.Close()
s.readSentence(t, "/ip/cloud/print @ [{`.proplist` `public-address,ddns-enabled`}]")
s.writeSentence(t, "!re", "=ddns-enabled="+state, "=public-address=0.0.0.0")
s.writeSentence(t, "!done")
}()

return c
}

// Heplers
type fakeServer struct {
r proto.Reader
w proto.Writer
io.Closer
}

type conn struct {
*io.PipeReader
*io.PipeWriter
}

func (c *conn) Close() error {
c.PipeReader.Close()
c.PipeWriter.Close()
return nil
}

func newPair(t *testing.T) (*routeros.Client, *fakeServer) {
ar, aw := io.Pipe()
br, bw := io.Pipe()

c, err := routeros.NewClient(&conn{ar, bw})
if err != nil {
t.Fatal(err)
}

s := &fakeServer{
proto.NewReader(br),
proto.NewWriter(aw),
&conn{br, aw},
}

return c, s
}

func (f *fakeServer) readSentence(t *testing.T, want string) {
sen, err := f.r.ReadSentence()
if err != nil {
t.Fatal(err)
}
if sen.String() != want {
t.Fatalf("Sentence (%s); want (%s)", sen.String(), want)
}
t.Logf("< %s\n", sen)
}

func (f *fakeServer) writeSentence(t *testing.T, sentence ...string) {
t.Logf("> %#q\n", sentence)
f.w.BeginSentence()
for _, word := range sentence {
f.w.WriteWord(word)
}
err := f.w.EndSentence()
if err != nil {
t.Fatal(err)
}
}
7 changes: 7 additions & 0 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ func WithNetwatch() Option {
}
}

// WithCloud enables cloud service metrics
func WithCloud() Option {
return func(c *collector) {
c.collectors = append(c.collectors, newCloudCollector())
}
}

// Option applies options to collector
type Option func(*collector)

Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Config struct {
Ipsec bool `yaml:"ipsec,omitempty"`
Lte bool `yaml:"lte,omitempty"`
Netwatch bool `yaml:"netwatch,omitempty"`
Cloud bool `yaml:"netwatch,omitempty"`
} `yaml:"features,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions config/config.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ features:
ipsec: true
lte: true
netwatch: true
cloud: true
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestShouldParse(t *testing.T) {
assertFeature("Ipsec", c.Features.Ipsec, t)
assertFeature("Lte", c.Features.Lte, t)
assertFeature("Netwatch", c.Features.Netwatch, t)
assertFeature("Cloud", c.Features.Cloud, t)
}

func loadTestFile(t *testing.T) []byte {
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var (
withIpsec = flag.Bool("with-ipsec", false, "retrieves ipsec metrics")
withLte = flag.Bool("with-lte", false, "retrieves lte metrics")
withNetwatch = flag.Bool("with-netwatch", false, "retrieves netwatch metrics")
withCloud = flag.Bool("with-cloud", false, "retrives cloud services stats")

cfg *config.Config

Expand Down Expand Up @@ -274,6 +275,10 @@ func collectorOptions() []collector.Option {
opts = append(opts, collector.WithNetwatch())
}

if *withCloud || cfg.Features.Cloud {
opts = append(opts, collector.WithCloud())
}

if *timeout != collector.DefaultTimeout {
opts = append(opts, collector.WithTimeout(*timeout))
}
Expand Down