fix: add normalization of tags for ethtool input plugin (#9901)
This commit is contained in:
parent
47301e6ef4
commit
3e1ebdb4c7
|
|
@ -12,6 +12,15 @@ The ethtool input plugin pulls ethernet device stats. Fields pulled will depend
|
||||||
|
|
||||||
## List of interfaces to ignore when pulling metrics.
|
## List of interfaces to ignore when pulling metrics.
|
||||||
# interface_exclude = ["eth1"]
|
# 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:
|
Interfaces can be included or ignored using:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ type Ethtool struct {
|
||||||
// This is the list of interface names to ignore
|
// This is the list of interface names to ignore
|
||||||
InterfaceExclude []string `toml:"interface_exclude"`
|
InterfaceExclude []string `toml:"interface_exclude"`
|
||||||
|
|
||||||
|
// Normalization on the key names
|
||||||
|
NormalizeKeys []string `toml:"normalize_keys"`
|
||||||
|
|
||||||
Log telegraf.Logger `toml:"-"`
|
Log telegraf.Logger `toml:"-"`
|
||||||
|
|
||||||
// the ethtool command
|
// the ethtool command
|
||||||
|
|
@ -38,6 +41,15 @@ const (
|
||||||
|
|
||||||
## List of interfaces to ignore when pulling metrics.
|
## List of interfaces to ignore when pulling metrics.
|
||||||
# interface_exclude = ["eth1"]
|
# 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"]
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ package ethtool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -81,12 +83,53 @@ func (e *Ethtool) gatherEthtoolStats(iface net.Interface, acc telegraf.Accumulat
|
||||||
|
|
||||||
fields[fieldInterfaceUp] = e.interfaceUp(iface)
|
fields[fieldInterfaceUp] = e.interfaceUp(iface)
|
||||||
for k, v := range stats {
|
for k, v := range stats {
|
||||||
fields[k] = v
|
fields[e.normalizeKey(k)] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.AddFields(pluginName, fields, tags)
|
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 {
|
func (e *Ethtool) interfaceUp(iface net.Interface) bool {
|
||||||
return (iface.Flags & net.FlagUp) != 0
|
return (iface.Flags & net.FlagUp) != 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,3 +380,119 @@ func TestGatherIgnoreInterfaces(t *testing.T) {
|
||||||
}
|
}
|
||||||
acc.AssertContainsTaggedFields(t, pluginName, expectedFieldsEth2, expectedTagsEth2)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue