Skip to content

Commit

Permalink
Add type to parse date and time (#1234)
Browse files Browse the repository at this point in the history
Add new type ParseDateAndTime to parse timestamps from DisplayString
and report it as unix timestamp.

* Add type to parse date and time (fixes: #1232)
* Change date and time parsing to use strptime format

---------

Signed-off-by: PhiBo <phibo@dinotools.org>
  • Loading branch information
phibos authored Aug 30, 2024
1 parent 23680cc commit 1d75633
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 17 deletions.
17 changes: 17 additions & 0 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/gosnmp/gosnmp"
"github.com/itchyny/timefmt-go"
"github.com/prometheus/client_golang/prometheus"

"github.com/prometheus/snmp_exporter/config"
Expand Down Expand Up @@ -544,6 +545,15 @@ func parseDateAndTime(pdu *gosnmp.SnmpPDU) (float64, error) {
return float64(t.Unix()), nil
}

func parseDateAndTimeWithPattern(metric *config.Metric, pdu *gosnmp.SnmpPDU, metrics Metrics) (float64, error) {
pduValue := pduValueAsString(pdu, "DisplayString", metrics)
t, err := timefmt.Parse(pduValue, metric.DateTimePattern)
if err != nil {
return 0, fmt.Errorf("error parsing date and time %q", err)
}
return float64(t.Unix()), nil
}

func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, oidToPdu map[string]gosnmp.SnmpPDU, logger log.Logger, metrics Metrics) []prometheus.Metric {
var err error
// The part of the OID that is the indexes.
Expand Down Expand Up @@ -573,6 +583,13 @@ func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, o
level.Debug(logger).Log("msg", "Error parsing DateAndTime", "err", err)
return []prometheus.Metric{}
}
case "ParseDateAndTime":
t = prometheus.GaugeValue
value, err = parseDateAndTimeWithPattern(metric, pdu, metrics)
if err != nil {
level.Debug(logger).Log("msg", "Error parsing ParseDateAndTime", "err", err)
return []prometheus.Metric{}
}
case "EnumAsInfo":
return enumAsInfo(metric, int(value), labelnames, labelvalues)
case "EnumAsStateSet":
Expand Down
34 changes: 34 additions & 0 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,40 @@ func TestParseDateAndTime(t *testing.T) {
}
}

func TestParseDateAndTimeWithPattern(t *testing.T) {
cases := []struct {
pdu *gosnmp.SnmpPDU
metric config.Metric
result float64
shouldErr bool
}{
{
pdu: &gosnmp.SnmpPDU{Value: "Apr 01 2025"},
metric: config.Metric{DateTimePattern: "%b %d %Y"},
result: 1.7434656e+09,
shouldErr: false,
},
{
pdu: &gosnmp.SnmpPDU{Value: "ABC"},
metric: config.Metric{DateTimePattern: "%b %d %Y"},
result: 0,
shouldErr: true,
},
}
for _, c := range cases {
got, err := parseDateAndTimeWithPattern(&c.metric, c.pdu, Metrics{})
if c.shouldErr && err == nil {
t.Fatalf("Was expecting error, but none returned.")
}
if !c.shouldErr && err != nil {
t.Fatalf("Was expecting no error, but one returned.")
}
if !reflect.DeepEqual(got, c.result) {
t.Errorf("parseDateAndTime(%v) result: got %v, want %v", c.pdu, got, c.result)
}
}
}

func TestIndexesToLabels(t *testing.T) {
cases := []struct {
oid []int
Expand Down
21 changes: 11 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,17 @@ type DynamicFilter struct {
}

type Metric struct {
Name string `yaml:"name"`
Oid string `yaml:"oid"`
Type string `yaml:"type"`
Help string `yaml:"help"`
Indexes []*Index `yaml:"indexes,omitempty"`
Lookups []*Lookup `yaml:"lookups,omitempty"`
RegexpExtracts map[string][]RegexpExtract `yaml:"regex_extracts,omitempty"`
EnumValues map[int]string `yaml:"enum_values,omitempty"`
Offset float64 `yaml:"offset,omitempty"`
Scale float64 `yaml:"scale,omitempty"`
Name string `yaml:"name"`
Oid string `yaml:"oid"`
Type string `yaml:"type"`
Help string `yaml:"help"`
Indexes []*Index `yaml:"indexes,omitempty"`
Lookups []*Lookup `yaml:"lookups,omitempty"`
RegexpExtracts map[string][]RegexpExtract `yaml:"regex_extracts,omitempty"`
DateTimePattern string `yaml:"datetime_pattern,omitempty"`
EnumValues map[int]string `yaml:"enum_values,omitempty"`
Offset float64 `yaml:"offset,omitempty"`
Scale float64 `yaml:"scale,omitempty"`
}

type Index struct {
Expand Down
2 changes: 2 additions & 0 deletions generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ modules:
value: '1' # The first entry whose regex matches and whose value parses wins.
- regex: '.*'
value: '0'
datetime_pattern: # Used if type = ParseDateAndTime. Uses the strptime format (See: man 3 strptime)
offset: 1.0 # Add the value to the same. Applied after scale.
scale: 1.0 # Scale the value of the sample by this value.
type: DisplayString # Override the metric type, possible types are:
# gauge: An integer with type gauge.
# counter: An integer with type counter.
# OctetString: A bit string, rendered as 0xff34.
# DateAndTime: An RFC 2579 DateAndTime byte sequence. If the device has no time zone data, UTC is used.
# ParseDateAndTime: Parse a DisplayString and return the timestamp. See datetime_pattern config option
# DisplayString: An ASCII or UTF-8 string.
# PhysAddress48: A 48 bit MAC address, rendered as 00:01:02:03:04:ff.
# Float: A 32 bit floating-point value with type gauge.
Expand Down
16 changes: 9 additions & 7 deletions generator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ package main

import (
"fmt"
"github.com/prometheus/snmp_exporter/config"
"strconv"

"github.com/prometheus/snmp_exporter/config"
)

// The generator config.
Expand All @@ -27,12 +28,13 @@ type Config struct {
}

type MetricOverrides struct {
Ignore bool `yaml:"ignore,omitempty"`
RegexpExtracts map[string][]config.RegexpExtract `yaml:"regex_extracts,omitempty"`
Offset float64 `yaml:"offset,omitempty"`
Scale float64 `yaml:"scale,omitempty"`
Type string `yaml:"type,omitempty"`
Help string `yaml:"help,omitempty"`
Ignore bool `yaml:"ignore,omitempty"`
RegexpExtracts map[string][]config.RegexpExtract `yaml:"regex_extracts,omitempty"`
DateTimePattern string `yaml:"datetime_pattern,omitempty"`
Offset float64 `yaml:"offset,omitempty"`
Scale float64 `yaml:"scale,omitempty"`
Type string `yaml:"type,omitempty"`
Help string `yaml:"help,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
Expand Down
6 changes: 6 additions & 0 deletions generator/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ func prepareTree(nodes *Node, logger log.Logger) map[string]*Node {
if n.TextualConvention == "DateAndTime" {
n.Type = "DateAndTime"
}
if n.TextualConvention == "ParseDateAndTime" {
n.Type = "ParseDateAndTime"
}
// Convert RFC 4001 InetAddress types textual convention to type.
if n.TextualConvention == "InetAddressIPv4" || n.TextualConvention == "InetAddressIPv6" || n.TextualConvention == "InetAddress" {
n.Type = n.TextualConvention
Expand Down Expand Up @@ -167,6 +170,8 @@ func metricType(t string) (string, bool) {
return t, true
case "DateAndTime":
return t, true
case "ParseDateAndTime":
return t, true
case "EnumAsInfo", "EnumAsStateSet":
return t, true
default:
Expand Down Expand Up @@ -528,6 +533,7 @@ func generateConfigModule(cfg *ModuleConfig, node *Node, nameToNode map[string]*
for _, metric := range out.Metrics {
if name == metric.Name || name == metric.Oid {
metric.RegexpExtracts = params.RegexpExtracts
metric.DateTimePattern = params.DateTimePattern
metric.Offset = params.Offset
metric.Scale = params.Scale
if params.Help != "" {
Expand Down
12 changes: 12 additions & 0 deletions generator/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ func TestTreePrepare(t *testing.T) {
in: &Node{Oid: "1", Type: "DisplayString", TextualConvention: "DateAndTime"},
out: &Node{Oid: "1", Type: "DateAndTime", TextualConvention: "DateAndTime"},
},
// ParseDateAndTime
{
in: &Node{Oid: "1", Type: "DisplayString", TextualConvention: "ParseDateAndTime"},
out: &Node{Oid: "1", Type: "ParseDateAndTime", TextualConvention: "ParseDateAndTime"},
},
// RFC 4100 InetAddress conventions.
{
in: &Node{Oid: "1", Type: "OctectString", TextualConvention: "InetAddressIPv4"},
Expand Down Expand Up @@ -340,6 +345,7 @@ func TestGenerateConfigModule(t *testing.T) {
{Oid: "1.202", Access: "ACCESS_READONLY", Label: "DateAndTime", Type: "DisplayString", TextualConvention: "DateAndTime"},
{Oid: "1.203", Access: "ACCESS_READONLY", Label: "InetAddressIPv4", Type: "OCTETSTR", TextualConvention: "InetAddressIPv4"},
{Oid: "1.204", Access: "ACCESS_READONLY", Label: "InetAddressIPv6", Type: "OCTETSTR", TextualConvention: "InetAddressIPv6"},
{Oid: "1.205", Access: "ACCESS_READONLY", Label: "ParseDateAndTime", Type: "DisplayString", TextualConvention: "ParseDateAndTime"},
}},
cfg: &ModuleConfig{
Walk: []string{"root", "1.3"},
Expand Down Expand Up @@ -461,6 +467,12 @@ func TestGenerateConfigModule(t *testing.T) {
Type: "InetAddressIPv6",
Help: " - 1.204",
},
{
Name: "ParseDateAndTime",
Oid: "1.205",
Type: "ParseDateAndTime",
Help: " - 1.205",
},
},
},
},
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/go-kit/log v0.2.1
github.com/gosnmp/gosnmp v1.37.0
github.com/itchyny/timefmt-go v0.1.6
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.55.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gosnmp/gosnmp v1.37.0 h1:/Tf8D3b9wrnNuf/SfbvO+44mPrjVphBhRtcGg22V07Y=
github.com/gosnmp/gosnmp v1.37.0/go.mod h1:GDH9vNqpsD7f2HvZhKs5dlqSEcAS6s6Qp099oZRCR+M=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down

0 comments on commit 1d75633

Please sign in to comment.