diff --git a/collector/collector.go b/collector/collector.go index e4e3fef8..522b7ab8 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -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" @@ -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. @@ -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": diff --git a/collector/collector_test.go b/collector/collector_test.go index 5582ad4e..df1b6ded 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -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 diff --git a/config/config.go b/config/config.go index f6464a1c..e2f531d0 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/generator/README.md b/generator/README.md index 216d3e30..4c3fd1ed 100644 --- a/generator/README.md +++ b/generator/README.md @@ -158,6 +158,7 @@ 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: @@ -165,6 +166,7 @@ modules: # 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. diff --git a/generator/config.go b/generator/config.go index f52e35ad..262aceb7 100644 --- a/generator/config.go +++ b/generator/config.go @@ -15,8 +15,9 @@ package main import ( "fmt" - "github.com/prometheus/snmp_exporter/config" "strconv" + + "github.com/prometheus/snmp_exporter/config" ) // The generator config. @@ -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. diff --git a/generator/tree.go b/generator/tree.go index f7fceb91..256cddc3 100644 --- a/generator/tree.go +++ b/generator/tree.go @@ -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 @@ -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: @@ -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 != "" { diff --git a/generator/tree_test.go b/generator/tree_test.go index f5941ceb..297073a3 100644 --- a/generator/tree_test.go +++ b/generator/tree_test.go @@ -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"}, @@ -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"}, @@ -461,6 +467,12 @@ func TestGenerateConfigModule(t *testing.T) { Type: "InetAddressIPv6", Help: " - 1.204", }, + { + Name: "ParseDateAndTime", + Oid: "1.205", + Type: "ParseDateAndTime", + Help: " - 1.205", + }, }, }, }, diff --git a/go.mod b/go.mod index 8414af5c..9259b9a4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index fe77ad49..60e48751 100644 --- a/go.sum +++ b/go.sum @@ -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=