Skip to content

Commit 3e1ebdb

Browse files
authored
fix: add normalization of tags for ethtool input plugin (#9901)
1 parent 47301e6 commit 3e1ebdb

4 files changed

Lines changed: 181 additions & 1 deletion

File tree

plugins/inputs/ethtool/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ The ethtool input plugin pulls ethernet device stats. Fields pulled will depend
1212

1313
## List of interfaces to ignore when pulling metrics.
1414
# interface_exclude = ["eth1"]
15+
16+
## Some drivers declare statistics with extra whitespace, different spacing,
17+
## and mix cases. This list, when enabled, can be used to clean the keys.
18+
## Here are the current possible normalizations:
19+
## * snakecase: converts fooBarBaz to foo_bar_baz
20+
## * trim: removes leading and trailing whitespace
21+
## * lower: changes all capitalized letters to lowercase
22+
## * underscore: replaces spaces with underscores
23+
# normalize_keys = ["snakecase", "trim", "lower", "underscore"]
1524
```
1625

1726
Interfaces can be included or ignored using:

plugins/inputs/ethtool/ethtool.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ type Ethtool struct {
2020
// This is the list of interface names to ignore
2121
InterfaceExclude []string `toml:"interface_exclude"`
2222

23+
// Normalization on the key names
24+
NormalizeKeys []string `toml:"normalize_keys"`
25+
2326
Log telegraf.Logger `toml:"-"`
2427

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

plugins/inputs/ethtool/ethtool_linux.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package ethtool
55

66
import (
77
"net"
8+
"regexp"
9+
"strings"
810
"sync"
911

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

8284
fields[fieldInterfaceUp] = e.interfaceUp(iface)
8385
for k, v := range stats {
84-
fields[k] = v
86+
fields[e.normalizeKey(k)] = v
8587
}
8688

8789
acc.AddFields(pluginName, fields, tags)
8890
}
8991

92+
// normalize key string; order matters to avoid replacing whitespace with
93+
// underscores, then trying to trim those same underscores. Likewise with
94+
// camelcase before trying to lower case things.
95+
func (e *Ethtool) normalizeKey(key string) string {
96+
// must trim whitespace or this will have a leading _
97+
if inStringSlice(e.NormalizeKeys, "snakecase") {
98+
key = camelCase2SnakeCase(strings.TrimSpace(key))
99+
}
100+
// must occur before underscore, otherwise nothing to trim
101+
if inStringSlice(e.NormalizeKeys, "trim") {
102+
key = strings.TrimSpace(key)
103+
}
104+
if inStringSlice(e.NormalizeKeys, "lower") {
105+
key = strings.ToLower(key)
106+
}
107+
if inStringSlice(e.NormalizeKeys, "underscore") {
108+
key = strings.ReplaceAll(key, " ", "_")
109+
}
110+
111+
return key
112+
}
113+
114+
func camelCase2SnakeCase(value string) string {
115+
matchFirstCap := regexp.MustCompile("(.)([A-Z][a-z]+)")
116+
matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])")
117+
118+
snake := matchFirstCap.ReplaceAllString(value, "${1}_${2}")
119+
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
120+
return strings.ToLower(snake)
121+
}
122+
123+
func inStringSlice(slice []string, value string) bool {
124+
for _, item := range slice {
125+
if item == value {
126+
return true
127+
}
128+
}
129+
130+
return false
131+
}
132+
90133
func (e *Ethtool) interfaceUp(iface net.Interface) bool {
91134
return (iface.Flags & net.FlagUp) != 0
92135
}

plugins/inputs/ethtool/ethtool_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,119 @@ func TestGatherIgnoreInterfaces(t *testing.T) {
380380
}
381381
acc.AssertContainsTaggedFields(t, pluginName, expectedFieldsEth2, expectedTagsEth2)
382382
}
383+
384+
type TestCase struct {
385+
normalization []string
386+
stats map[string]uint64
387+
expectedFields map[string]uint64
388+
}
389+
390+
func TestNormalizedKeys(t *testing.T) {
391+
cases := []TestCase{
392+
{
393+
normalization: []string{"underscore"},
394+
stats: map[string]uint64{
395+
"port rx": 1,
396+
" Port_tx": 0,
397+
"interface_up": 0,
398+
},
399+
expectedFields: map[string]uint64{
400+
"port_rx": 1,
401+
"_Port_tx": 0,
402+
"interface_up": 0,
403+
},
404+
},
405+
{
406+
normalization: []string{"underscore", "lower"},
407+
stats: map[string]uint64{
408+
"Port rx": 1,
409+
" Port_tx": 0,
410+
"interface_up": 0,
411+
},
412+
expectedFields: map[string]uint64{
413+
"port_rx": 1,
414+
"_port_tx": 0,
415+
"interface_up": 0,
416+
},
417+
},
418+
{
419+
normalization: []string{"underscore", "lower", "trim"},
420+
stats: map[string]uint64{
421+
" Port RX ": 1,
422+
" Port_tx": 0,
423+
"interface_up": 0,
424+
},
425+
expectedFields: map[string]uint64{
426+
"port_rx": 1,
427+
"port_tx": 0,
428+
"interface_up": 0,
429+
},
430+
},
431+
{
432+
normalization: []string{"underscore", "lower", "snakecase", "trim"},
433+
stats: map[string]uint64{
434+
" Port RX ": 1,
435+
" Port_tx": 0,
436+
"interface_up": 0,
437+
},
438+
expectedFields: map[string]uint64{
439+
"port_rx": 1,
440+
"port_tx": 0,
441+
"interface_up": 0,
442+
},
443+
},
444+
{
445+
normalization: []string{"snakecase"},
446+
stats: map[string]uint64{
447+
" PortRX ": 1,
448+
" PortTX": 0,
449+
"interface_up": 0,
450+
},
451+
expectedFields: map[string]uint64{
452+
"port_rx": 1,
453+
"port_tx": 0,
454+
"interface_up": 0,
455+
},
456+
},
457+
{
458+
normalization: []string{},
459+
stats: map[string]uint64{
460+
" Port RX ": 1,
461+
" Port_tx": 0,
462+
"interface_up": 0,
463+
},
464+
expectedFields: map[string]uint64{
465+
" Port RX ": 1,
466+
" Port_tx": 0,
467+
"interface_up": 0,
468+
},
469+
},
470+
}
471+
for _, c := range cases {
472+
eth0 := &InterfaceMock{"eth0", "e1000e", c.stats, false, true}
473+
expectedTags := map[string]string{
474+
"interface": eth0.Name,
475+
"driver": eth0.DriverName,
476+
}
477+
478+
interfaceMap = make(map[string]*InterfaceMock)
479+
interfaceMap[eth0.Name] = eth0
480+
481+
cmd := &CommandEthtoolMock{interfaceMap}
482+
command = &Ethtool{
483+
InterfaceInclude: []string{},
484+
InterfaceExclude: []string{},
485+
NormalizeKeys: c.normalization,
486+
command: cmd,
487+
}
488+
489+
var acc testutil.Accumulator
490+
err := command.Gather(&acc)
491+
492+
assert.NoError(t, err)
493+
assert.Len(t, acc.Metrics, 1)
494+
495+
acc.AssertContainsFields(t, pluginName, toStringMapInterface(c.expectedFields))
496+
acc.AssertContainsTaggedFields(t, pluginName, toStringMapInterface(c.expectedFields), expectedTags)
497+
}
498+
}

0 commit comments

Comments
 (0)