Skip to content

Commit

Permalink
Merge pull request #69 from maier/master
Browse files Browse the repository at this point in the history
v0.0.45
  • Loading branch information
maier authored May 26, 2022
2 parents fe1d2c4 + 9a1b6a7 commit c1d8602
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 36 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.0.45

* add: (snmp) `timestamp` conversion for OIDs returning date/time strings (requires `timestamp_layout` to be set) [CIRC-8420]

# v0.0.44

* upd: go-trapmetrrics v0.0.8
Expand Down
30 changes: 19 additions & 11 deletions plugins/inputs/snmp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,31 @@ option operate similar to the `snmpget` utility.
# is_tag = false

## Apply one of the following conversions to the variable value:
## float(X) Convert the input value into a float and divides by the
## Xth power of 10. Effectively just moves the decimal left
## X places. For example a value of `123` with `float(2)`
## will result in `1.23`.
## float: Convert the value into a float with no adjustment. Same
## as `float(0)`.
## int: Convert the value into an integer.
## hwaddr: Convert the value to a MAC address.
## ipaddr: Convert the value to an IP address.
## string: Force convert a byte slice to a string (nonprintable characters will be converted to '_').
## regexp: Use a regular expression to extract a value from the input.
## float(X) Convert the input value into a float and divides by the
## Xth power of 10. Effectively just moves the decimal left
## X places. For example a value of `123` with `float(2)`
## will result in `1.23`.
## float: Convert the value into a float with no adjustment. Same
## as `float(0)`.
## int: Convert the value into an integer.
## hwaddr: Convert the value to a MAC address.
## ipaddr: Convert the value to an IP address.
## string: Force convert a byte slice to a string (nonprintable characters will be converted to '_').
## regexp: Use a regular expression to extract a value from the input.
## timestamp: Parse the input as a timestamp and convert it to a unix epoch in UTC.
# conversion = ""
## Regular expression used to extract value from input.
## Must contain a pattern named 'value' e.g. (?P<value>re).
# regexp = ""
## Type of the value to be extracted from the input. (float|int|uint|text)
# regexp_type = ""
## Timestamp laytout - format of the string to be parsed as a timestamp.
## e.g.
## timestamp from oid timestamp_layout
## "07/13/2025" "01/02/2006"
## "03/03/2022 23:49:22" "01/02/2006 15:04:05"
## See: https://pkg.go.dev/time#pkg-constants for more example layouts
# timestamp_layout = ""
```

#### Table
Expand Down
66 changes: 45 additions & 21 deletions plugins/inputs/snmp/snmp.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,15 @@ type Field struct {
// otherwise it will encoded as hex
// if it is not a byte slice, it will be returned as-is
// "regexp" Use a regular expression to extract a value from the input.
// "timestamp" string treated as a timestamp and parsed into unix epoch based on timestamp_layout pattern
Conversion string
// Regular expression used to extract value from input.
// Must contain a pattern named 'value' e.g. (?P<value>re).
Regexp string
// Type of the value to be extracted from the input. (float|int|uint|text)
RegexpType string
// Timestamp layout
TimestampLayout string
// OidIndexLength specifies the length of the index in OID path segments. It can be used to remove sub-identifiers that vary in content or length.
OidIndexLength int
// IsTag controls whether this OID is output as a tag or a value.
Expand Down Expand Up @@ -598,7 +601,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
return nil, fmt.Errorf("performing get on field %s (oid:%s): %w", f.Name, oid, err)
} else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance {
ent := pkt.Variables[0]
fv, err := fieldConvert(f.Conversion, f.rx, f.RegexpType, ent)
fv, err := fieldConvert(f, ent)
if err != nil {
return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err)
}
Expand Down Expand Up @@ -643,7 +646,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
}

if f.TextMetric && f.Conversion == "lookup" {
fv, err := fieldConvert("int", nil, "", ent)
fv, err := fieldConvert(Field{Conversion: "int"}, ent)
if err != nil {
return &walkError{
msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
Expand All @@ -652,7 +655,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
}
ifv[idx] = fv

fv, err = fieldConvert(f.Conversion, f.rx, f.RegexpType, ent)
fv, err = fieldConvert(f, ent)
if err != nil {
return &walkError{
msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
Expand All @@ -662,7 +665,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
ifv[idx+"_desc"] = fv

} else {
fv, err := fieldConvert(f.Conversion, f.rx, f.RegexpType, ent)
fv, err := fieldConvert(f, ent)
if err != nil {
return &walkError{
msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
Expand Down Expand Up @@ -799,10 +802,13 @@ func (s *Snmp) getConnection(idx int) (snmpConnection, error) {
// "" will convert a byte slice into a string (if all runes are printable, otherwise it will return a hex string)
// if the value is not a byte slice, it is returned as-is
// "regex" will extract a value from the input
func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpPDU) (interface{}, error) {
// "timestamp" convert string to unix epoch based on timestamp_layout
// see: https://pkg.go.dev/time#pkg-constants and https://pkg.go.dev/time#Parse
func fieldConvert(f Field, sv gosnmp.SnmpPDU) (interface{}, error) {

v := sv.Value

if conv == "" {
if f.Conversion == "" {
if bs, ok := v.([]byte); ok {
for _, b := range bs {
if !unicode.IsPrint(rune(b)) {
Expand All @@ -814,7 +820,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

if conv == "string" {
if f.Conversion == "string" {
if bs, ok := v.([]byte); ok {
str := strings.Map(func(r rune) rune {
if unicode.IsPrint(r) {
Expand All @@ -827,8 +833,8 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

if conv == "regexp" {
if rx == nil {
if f.Conversion == "regexp" {
if f.rx == nil {
return "", fmt.Errorf("regexp didn't compile, check log for errors")
}

Expand All @@ -842,14 +848,14 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return "", fmt.Errorf("unknown input type (%T) for regex", v)
}

if rx.MatchString(input) {
valIdx := rx.SubexpIndex("value")
matches := rx.FindStringSubmatch(input)
if f.rx.MatchString(input) {
valIdx := f.rx.SubexpIndex("value")
matches := f.rx.FindStringSubmatch(input)
if len(matches) < valIdx {
return "", fmt.Errorf("no match found for regex %q %q", input, rx.String())
return "", fmt.Errorf("no match found for regex %q %q", input, f.rx.String())
}
match := matches[valIdx]
switch rxtype {
switch f.RegexpType {
case "float":
fv, err := strconv.ParseFloat(match, 64)
return fv, err
Expand All @@ -864,10 +870,28 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
}
}

return "", fmt.Errorf("no value found for regex %q %q", input, rx.String())
return "", fmt.Errorf("no value found for regex %q %q", input, f.rx.String())
}

if f.Conversion == "timestamp" {
if f.TimestampLayout == "" {
return "", fmt.Errorf("no timestamp_layout provided for 'timestamp' conversion")
}

vs, isString := v.(string)
if !isString {
return "", fmt.Errorf("could not convert to string %v", v)
}

ts, err := time.Parse(f.TimestampLayout, vs)
if err != nil {
return "", fmt.Errorf("converting (%s): %w", vs, err)
}

return ts.UTC().Unix(), nil
}

if conv == "lookup" {
if f.Conversion == "lookup" {
stc := TranslateOID(sv.Name)
key := fmt.Sprintf("%d", v)
if stc.valMap != nil {
Expand All @@ -880,7 +904,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
}

var d int
if _, err := fmt.Sscanf(conv, "float(%d)", &d); err == nil || conv == "float" {
if _, err := fmt.Sscanf(f.Conversion, "float(%d)", &d); err == nil || f.Conversion == "float" {
switch vt := v.(type) {
case float32:
v = float64(vt) / math.Pow10(d)
Expand Down Expand Up @@ -916,7 +940,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

if conv == "int" {
if f.Conversion == "int" {
switch vt := v.(type) {
case float32:
v = int64(vt)
Expand Down Expand Up @@ -950,7 +974,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

if conv == "hwaddr" {
if f.Conversion == "hwaddr" {
switch vt := v.(type) {
case string:
v = net.HardwareAddr(vt).String()
Expand All @@ -962,7 +986,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

if conv == "ipaddr" {
if f.Conversion == "ipaddr" {
var ipbs []byte

switch vt := v.(type) {
Expand All @@ -984,7 +1008,7 @@ func fieldConvert(conv string, rx *regexp.Regexp, rxtype string, sv gosnmp.SnmpP
return v, nil
}

return nil, fmt.Errorf("invalid conversion type '%s'", conv)
return nil, fmt.Errorf("invalid conversion type '%s'", f.Conversion)
}

type snmpTableCache struct {
Expand Down
66 changes: 62 additions & 4 deletions plugins/inputs/snmp/snmp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,8 @@ func TestFieldConvert(t *testing.T) {
input gosnmp.SnmpPDU
}{
{input: gosnmp.SnmpPDU{Value: []byte("foo")}, conv: "", expected: string("foo")},
{input: gosnmp.SnmpPDU{Value: []byte("foo\u00a0")}, conv: "", expected: string("666f6fc2a0")},
{input: gosnmp.SnmpPDU{Value: []byte("foo\u00a0")}, conv: "string", expected: string("foo_")},
{input: gosnmp.SnmpPDU{Value: []byte("foo\x07")}, conv: "", expected: string("666f6f07")},
{input: gosnmp.SnmpPDU{Value: []byte("foo\x07")}, conv: "string", expected: string("foo_")},
{input: gosnmp.SnmpPDU{Value: "0.123"}, conv: "float", expected: float64(0.123)},
{input: gosnmp.SnmpPDU{Value: []byte("0.123")}, conv: "float", expected: float64(0.123)},
{input: gosnmp.SnmpPDU{Value: float32(0.123)}, conv: "float", expected: float64(float32(0.123))},
Expand Down Expand Up @@ -750,7 +750,7 @@ func TestFieldConvert(t *testing.T) {
}

for _, tc := range testTable {
act, err := fieldConvert(tc.conv, nil, "", tc.input)
act, err := fieldConvert(Field{Conversion: tc.conv}, tc.input)
if !assert.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) {
continue
}
Expand Down Expand Up @@ -789,13 +789,71 @@ func TestFieldConvert(t *testing.T) {
},
}
for _, tst := range rxTests {
v, err := fieldConvert("regexp", tst.rx, tst.rt, tst.input)
v, err := fieldConvert(Field{
Conversion: "regexp",
rx: tst.rx,
RegexpType: tst.rt,
}, tst.input)
if !assert.NoError(t, err, "input=%T(%v) rxtype=%s expected=%T(%v)", tst.input, tst.input, tst.rt, tst.expected, tst.expected) {
continue
}
assert.EqualValues(t, tst.expected, v, "input=%T(%v) rxtype=%s expected=%T(%v)", tst.input, tst.input, tst.rt, tst.expected, tst.expected)
}

timestampTests := []struct {
expected interface{}
layout string
input gosnmp.SnmpPDU
wantErr bool
}{
{
input: gosnmp.SnmpPDU{Value: "07/13/2025"}, // CIRC-8420
layout: "01/02/2006",
expected: uint64(1752364800),
wantErr: false,
},
{
input: gosnmp.SnmpPDU{Value: "03/03/2022 23:49:22"}, // CIRC-8420
layout: "01/02/2006 15:04:05",
expected: uint64(1646351362),
wantErr: false,
},
{
input: gosnmp.SnmpPDU{Value: "03 Mar 22 23:49 MST"},
layout: "02 Jan 06 15:04 MST", // RFC822
expected: uint64(1646351340),
wantErr: false,
},
{
input: gosnmp.SnmpPDU{Value: "03 Mar 22 23:49 -0700"},
layout: "02 Jan 06 15:04 -0700", // RFC822Z
expected: uint64(1646376540),
wantErr: false,
},
{
input: gosnmp.SnmpPDU{Value: "2022-03-03T23:49:22Z05:00"},
layout: "2006-01-02T15:04:05Z05:00", // RFC339
expected: uint64(1646351345),
wantErr: false,
},
{
input: gosnmp.SnmpPDU{Value: "Thu Mar 3 23:48:22 2022"},
layout: "Mon Jan _2 15:04:05 2006", // ANSIC
expected: uint64(1646351302),
wantErr: false,
},
}
for _, tst := range timestampTests {
v, err := fieldConvert(Field{
Conversion: "timestamp",
TimestampLayout: tst.layout,
}, tst.input)
if !assert.NoError(t, err, "input=%T(%v) layout=%s expected=%T(%v)", tst.input, tst.input, tst.layout, tst.expected, tst.expected) {
continue
}
assert.EqualValues(t, tst.expected, v, "input=%T(%v) layout=%s expected=%T(%v)", tst.input, tst.input, tst.layout, tst.expected, tst.expected)
}

}

func TestSnmpTranslateCache_miss(t *testing.T) {
Expand Down

0 comments on commit c1d8602

Please sign in to comment.