diff --git a/collector/dhcp_lease_collector.go b/collector/dhcp_lease_collector.go index d8e8035..85b2bcc 100644 --- a/collector/dhcp_lease_collector.go +++ b/collector/dhcp_lease_collector.go @@ -1,10 +1,12 @@ package collector import ( + "strconv" + "strings" + "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "gopkg.in/routeros.v2/proto" - "strings" ) type dhcpLeaseCollector struct { @@ -13,9 +15,9 @@ type dhcpLeaseCollector struct { } func (c *dhcpLeaseCollector) init() { - c.props = []string{"active-mac-address", "status", "expires-after", "active-address", "host-name"} + c.props = []string{"active-mac-address", "server", "status", "expires-after", "active-address", "host-name"} - labelNames := []string{"name", "address", "activemacaddress", "status", "expiresafter", "activeaddress", "hostname"} + labelNames := []string{"name", "address", "activemacaddress", "server", "status", "expiresafter", "activeaddress", "hostname"} c.descriptions = description("dhcp", "leases_metrics", "number of metrics", labelNames) } @@ -44,7 +46,7 @@ func (c *dhcpLeaseCollector) collect(ctx *collectorContext) error { } func (c *dhcpLeaseCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, error) { - reply, err := ctx.client.Run("/ip/dhcp-server/lease/print", "=.proplist="+strings.Join(c.props, ",")) + reply, err := ctx.client.Run("/ip/dhcp-server/lease/print", "?status=bound", "=.proplist="+strings.Join(c.props, ",")) if err != nil { log.WithFields(log.Fields{ "device": ctx.device.Name, @@ -59,11 +61,22 @@ func (c *dhcpLeaseCollector) fetch(ctx *collectorContext) ([]*proto.Sentence, er func (c *dhcpLeaseCollector) collectMetric(ctx *collectorContext, re *proto.Sentence) { v := 1.0 + f, err := parseDuration(re.Map["expires-after"]) + if err != nil { + log.WithFields(log.Fields{ + "device": ctx.device.Name, + "property": "expires-after", + "value": re.Map["expires-after"], + "error": err, + }).Error("error parsing duration metric value") + return + } + activemacaddress := re.Map["active-mac-address"] + server := re.Map["server"] status := re.Map["status"] - expiresafter := re.Map["expires-after"] activeaddress := re.Map["active-address"] hostname := re.Map["host-name"] - ctx.ch <- prometheus.MustNewConstMetric(c.descriptions, prometheus.CounterValue, v, ctx.device.Name, ctx.device.Address, activemacaddress, status, expiresafter, activeaddress, hostname) + ctx.ch <- prometheus.MustNewConstMetric(c.descriptions, prometheus.GaugeValue, v, ctx.device.Name, ctx.device.Address, activemacaddress, server, status, strconv.FormatFloat(f, 'f', 0, 64), activeaddress, hostname) } diff --git a/collector/helper.go b/collector/helper.go index 623875a..726b3be 100644 --- a/collector/helper.go +++ b/collector/helper.go @@ -1,13 +1,25 @@ package collector import ( + "fmt" "math" + "regexp" "strconv" "strings" + "time" "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" ) +var durationRegex *regexp.Regexp +var durationParts [5]time.Duration + +func init() { + durationRegex = regexp.MustCompile(`(?:(\d*)w)?(?:(\d*)d)?(?:(\d*)h)?(?:(\d*)m)?(?:(\d*)s)?`) + durationParts = [5]time.Duration{time.Hour * 168, time.Hour * 24, time.Hour, time.Minute, time.Second} +} + func metricStringCleanup(in string) string { return strings.Replace(in, "-", "_", -1) } @@ -49,3 +61,30 @@ func splitStringToFloats(metric string) (float64, float64, error) { } return m1, m2, nil } + +func parseDuration(duration string) (float64, error) { + var u time.Duration + + reMatch := durationRegex.FindAllStringSubmatch(duration, -1) + + // should get one and only one match back on the regex + if len(reMatch) != 1 { + return 0, fmt.Errorf("invalid duration value sent to regex") + } else { + for i, match := range reMatch[0] { + if match != "" && i != 0 { + v, err := strconv.Atoi(match) + if err != nil { + log.WithFields(log.Fields{ + "duration": duration, + "value": match, + "error": err, + }).Error("error parsing duration field value") + return float64(0), err + } + u += time.Duration(v) * durationParts[i-1] + } + } + } + return u.Seconds(), nil +} diff --git a/collector/helper_test.go b/collector/helper_test.go new file mode 100644 index 0000000..33fc722 --- /dev/null +++ b/collector/helper_test.go @@ -0,0 +1,137 @@ +package collector + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSplitStringToFloats(t *testing.T) { + var testCases = []struct { + input string + expected struct { + f1 float64 + f2 float64 + } + isNaN bool + hasError bool + }{ + { + "1.2,2.1", + struct { + f1 float64 + f2 float64 + }{ + 1.2, + 2.1, + }, + false, + false, + }, + { + input: "1.2,", + isNaN: true, + hasError: true, + }, + { + input: ",2.1", + isNaN: true, + hasError: true, + }, + { + "1.2,2.1,3.2", + struct { + f1 float64 + f2 float64 + }{ + 1.2, + 2.1, + }, + false, + false, + }, + { + input: "", + isNaN: true, + hasError: true, + }, + } + + for _, testCase := range testCases { + f1, f2, err := splitStringToFloats(testCase.input) + + switch testCase.hasError { + case true: + assert.Error(t, err) + case false: + assert.NoError(t, err) + } + + switch testCase.isNaN { + case true: + assert.True(t, math.IsNaN(f1)) + assert.True(t, math.IsNaN(f2)) + case false: + assert.Equal(t, testCase.expected.f1, f1) + assert.Equal(t, testCase.expected.f2, f2) + } + } +} + +func TestParseDuration(t *testing.T) { + var testCases = []struct { + input string + output float64 + hasError bool + }{ + { + "3d3h42m53s", + 272573, + false, + }, + { + "15w3d3h42m53s", + 9344573, + false, + }, + { + "42m53s", + 2573, + false, + }, + { + "7w6d9h34m", + 4786440, + false, + }, + { + "59", + 0, + true, + }, + { + "s", + 0, + false, + }, + { + "", + 0, + false, + }, + } + + for _, testCase := range testCases { + f, err := parseDuration(testCase.input) + + switch testCase.hasError { + case true: + assert.Error(t, err) + case false: + assert.NoError(t, err) + } + + assert.Equal(t, testCase.output, f) + } +}