Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: trim whitespace on ethtool input plugin #9901

Merged
merged 5 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
powersj marked this conversation as resolved.
Show resolved Hide resolved
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 {
powersj marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}