Skip to content

Commit

Permalink
fix: add normalization of tags for ethtool input plugin (#9901)
Browse files Browse the repository at this point in the history
  • Loading branch information
powersj authored Oct 19, 2021
1 parent 47301e6 commit 3e1ebdb
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 1 deletion.
9 changes: 9 additions & 0 deletions plugins/inputs/ethtool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The ethtool input plugin pulls ethernet device stats. Fields pulled will depend

## List of interfaces to ignore when pulling metrics.
# interface_exclude = ["eth1"]

## Some drivers declare statistics with extra whitespace, different spacing,
## and mix cases. This list, when enabled, can be used to clean the keys.
## Here are the current possible normalizations:
## * snakecase: converts fooBarBaz to foo_bar_baz
## * trim: removes leading and trailing whitespace
## * lower: changes all capitalized letters to lowercase
## * underscore: replaces spaces with underscores
# normalize_keys = ["snakecase", "trim", "lower", "underscore"]
```

Interfaces can be included or ignored using:
Expand Down
12 changes: 12 additions & 0 deletions plugins/inputs/ethtool/ethtool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type Ethtool struct {
// This is the list of interface names to ignore
InterfaceExclude []string `toml:"interface_exclude"`

// Normalization on the key names
NormalizeKeys []string `toml:"normalize_keys"`

Log telegraf.Logger `toml:"-"`

// the ethtool command
Expand All @@ -38,6 +41,15 @@ const (
## List of interfaces to ignore when pulling metrics.
# interface_exclude = ["eth1"]
## Some drivers declare statistics with extra whitespace, different spacing,
## and mix cases. This list, when enabled, can be used to clean the keys.
## Here are the current possible normalizations:
## * snakecase: converts fooBarBaz to foo_bar_baz
## * trim: removes leading and trailing whitespace
## * lower: changes all capitalized letters to lowercase
## * underscore: replaces spaces with underscores
# normalize_keys = ["snakecase", "trim", "lower", "underscore"]
`
)

Expand Down
45 changes: 44 additions & 1 deletion plugins/inputs/ethtool/ethtool_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package ethtool

import (
"net"
"regexp"
"strings"
"sync"

"github.com/pkg/errors"
Expand Down Expand Up @@ -81,12 +83,53 @@ func (e *Ethtool) gatherEthtoolStats(iface net.Interface, acc telegraf.Accumulat

fields[fieldInterfaceUp] = e.interfaceUp(iface)
for k, v := range stats {
fields[k] = v
fields[e.normalizeKey(k)] = v
}

acc.AddFields(pluginName, fields, tags)
}

// normalize key string; order matters to avoid replacing whitespace with
// underscores, then trying to trim those same underscores. Likewise with
// camelcase before trying to lower case things.
func (e *Ethtool) normalizeKey(key string) string {
// must trim whitespace or this will have a leading _
if inStringSlice(e.NormalizeKeys, "snakecase") {
key = camelCase2SnakeCase(strings.TrimSpace(key))
}
// must occur before underscore, otherwise nothing to trim
if inStringSlice(e.NormalizeKeys, "trim") {
key = strings.TrimSpace(key)
}
if inStringSlice(e.NormalizeKeys, "lower") {
key = strings.ToLower(key)
}
if inStringSlice(e.NormalizeKeys, "underscore") {
key = strings.ReplaceAll(key, " ", "_")
}

return key
}

func camelCase2SnakeCase(value string) string {
matchFirstCap := regexp.MustCompile("(.)([A-Z][a-z]+)")
matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])")

snake := matchFirstCap.ReplaceAllString(value, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}

func inStringSlice(slice []string, value string) bool {
for _, item := range slice {
if item == value {
return true
}
}

return false
}

func (e *Ethtool) interfaceUp(iface net.Interface) bool {
return (iface.Flags & net.FlagUp) != 0
}
Expand Down
116 changes: 116 additions & 0 deletions plugins/inputs/ethtool/ethtool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,119 @@ func TestGatherIgnoreInterfaces(t *testing.T) {
}
acc.AssertContainsTaggedFields(t, pluginName, expectedFieldsEth2, expectedTagsEth2)
}

type TestCase struct {
normalization []string
stats map[string]uint64
expectedFields map[string]uint64
}

func TestNormalizedKeys(t *testing.T) {
cases := []TestCase{
{
normalization: []string{"underscore"},
stats: map[string]uint64{
"port rx": 1,
" Port_tx": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
"port_rx": 1,
"_Port_tx": 0,
"interface_up": 0,
},
},
{
normalization: []string{"underscore", "lower"},
stats: map[string]uint64{
"Port rx": 1,
" Port_tx": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
"port_rx": 1,
"_port_tx": 0,
"interface_up": 0,
},
},
{
normalization: []string{"underscore", "lower", "trim"},
stats: map[string]uint64{
" Port RX ": 1,
" Port_tx": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
"port_rx": 1,
"port_tx": 0,
"interface_up": 0,
},
},
{
normalization: []string{"underscore", "lower", "snakecase", "trim"},
stats: map[string]uint64{
" Port RX ": 1,
" Port_tx": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
"port_rx": 1,
"port_tx": 0,
"interface_up": 0,
},
},
{
normalization: []string{"snakecase"},
stats: map[string]uint64{
" PortRX ": 1,
" PortTX": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
"port_rx": 1,
"port_tx": 0,
"interface_up": 0,
},
},
{
normalization: []string{},
stats: map[string]uint64{
" Port RX ": 1,
" Port_tx": 0,
"interface_up": 0,
},
expectedFields: map[string]uint64{
" Port RX ": 1,
" Port_tx": 0,
"interface_up": 0,
},
},
}
for _, c := range cases {
eth0 := &InterfaceMock{"eth0", "e1000e", c.stats, false, true}
expectedTags := map[string]string{
"interface": eth0.Name,
"driver": eth0.DriverName,
}

interfaceMap = make(map[string]*InterfaceMock)
interfaceMap[eth0.Name] = eth0

cmd := &CommandEthtoolMock{interfaceMap}
command = &Ethtool{
InterfaceInclude: []string{},
InterfaceExclude: []string{},
NormalizeKeys: c.normalization,
command: cmd,
}

var acc testutil.Accumulator
err := command.Gather(&acc)

assert.NoError(t, err)
assert.Len(t, acc.Metrics, 1)

acc.AssertContainsFields(t, pluginName, toStringMapInterface(c.expectedFields))
acc.AssertContainsTaggedFields(t, pluginName, toStringMapInterface(c.expectedFields), expectedTags)
}
}

0 comments on commit 3e1ebdb

Please sign in to comment.