feat(go.d/ethtool): collect module ddm info using ethtool (#19426)
Ilya Mashchenko committed
Jan 17, 2025 at 15:28 UTC
c8b3dd3f1a049a62904c687fd07ae97aa29d13b2
17 files changed
+1155
src/go/plugin/go.d/collector/ethtool/charts.go
new
+121
@@ -0,0 +1,121 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "fmt"
7
+
8
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
9
+)
10
+
11
+const (
12
+ prioModuleReceiverPowerDbm = module.Priority + iota
13
+ prioModuleLaserOutputPowerDbm
14
+ prioModuleLaserBiasCurrent
15
+ prioModuleTemperatureC
16
+ prioModuleVoltage
17
+)
18
+
19
+var ifaceModuleEepromCharts = module.Charts{
20
+ ifaceModuleReceiverPowerDbmChartTmpl.Copy(),
21
+ ifaceModuleLaserPowerDbmChartTmpl.Copy(),
22
+ ifaceModuleLaserBiasCurrentChartTmpl.Copy(),
23
+ ifaceModuleTempCelsiusChartTmpl.Copy(),
24
+ ifaceModuleVoltageChartTmpl.Copy(),
25
+}
26
+
27
+var (
28
+ ifaceModuleReceiverPowerDbmChartTmpl = module.Chart{
29
+ ID: "iface_%s_module_receiver_power_dbm",
30
+ Title: "Module Receiver Signal Average Optical Power",
31
+ Units: "dBm",
32
+ Fam: "optical module",
33
+ Ctx: "ethtool.optical_module_receiver_signal_power",
34
+ Priority: prioModuleReceiverPowerDbm,
35
+ Dims: module.Dims{
36
+ {ID: "iface_%s_receiver_signal_average_optical_power_dbm", Name: "rx_power", Div: precision},
37
+ },
38
+ }
39
+ ifaceModuleLaserPowerDbmChartTmpl = module.Chart{
40
+ ID: "iface_%s_module_laser_output_power_dbm",
41
+ Title: "Module Laser Output Power",
42
+ Units: "dBm",
43
+ Fam: "optical module",
44
+ Ctx: "ethtool.optical_module_laser_output_power",
45
+ Priority: prioModuleLaserOutputPowerDbm,
46
+ Dims: module.Dims{
47
+ {ID: "iface_%s_laser_output_power_dbm", Name: "tx_power", Div: precision},
48
+ },
49
+ }
50
+ ifaceModuleLaserBiasCurrentChartTmpl = module.Chart{
51
+ ID: "iface_%s_module_laser_bias_current",
52
+ Title: "Module Laser Bias Current",
53
+ Units: "mA",
54
+ Fam: "optical module",
55
+ Ctx: "ethtool.optical_module_laser_bias_current",
56
+ Priority: prioModuleLaserBiasCurrent,
57
+ Dims: module.Dims{
58
+ {ID: "iface_%s_laser_bias_current_ma", Name: "bias_current", Div: precision},
59
+ },
60
+ }
61
+ ifaceModuleTempCelsiusChartTmpl = module.Chart{
62
+ ID: "iface_%s_module_temperature_c",
63
+ Title: "Module Temperature",
64
+ Units: "Celsius",
65
+ Fam: "optical module",
66
+ Ctx: "ethtool.optical_module_temperature",
67
+ Priority: prioModuleTemperatureC,
68
+ Dims: module.Dims{
69
+ {ID: "iface_%s_module_temperature_c", Name: "temperature", Div: precision},
70
+ },
71
+ }
72
+ ifaceModuleVoltageChartTmpl = module.Chart{
73
+ ID: "iface_%s_module_voltage",
74
+ Title: "Module Voltage",
75
+ Units: "Volts",
76
+ Fam: "optical module",
77
+ Ctx: "ethtool.optical_module_voltage",
78
+ Priority: prioModuleVoltage,
79
+ Dims: module.Dims{
80
+ {ID: "iface_%s_module_voltage_v", Name: "voltage", Div: precision},
81
+ },
82
+ }
83
+)
84
+
85
+func (c *Collector) addModuleEepromCharts(iface string, eeprom *moduleEeprom) {
86
+ if eeprom == nil || eeprom.ddm == nil {
87
+ return
88
+ }
89
+
90
+ charts := ifaceModuleEepromCharts.Copy()
91
+
92
+ if eeprom.ddm.laserBiasMA == nil {
93
+ _ = charts.Remove(ifaceModuleLaserBiasCurrentChartTmpl.ID)
94
+ }
95
+ if eeprom.ddm.laserPowerDBM == nil {
96
+ _ = charts.Remove(ifaceModuleLaserPowerDbmChartTmpl.ID)
97
+ }
98
+ if eeprom.ddm.rxSignalPowerDBM == nil {
99
+ _ = charts.Remove(ifaceModuleLaserPowerDbmChartTmpl.ID)
100
+ }
101
+ if eeprom.ddm.tempC == nil {
102
+ _ = charts.Remove(ifaceModuleTempCelsiusChartTmpl.ID)
103
+ }
104
+ if eeprom.ddm.voltageV == nil {
105
+ _ = charts.Remove(ifaceModuleVoltageChartTmpl.ID)
106
+ }
107
+
108
+ for _, chart := range *charts {
109
+ chart.ID = fmt.Sprintf(chart.ID, iface)
110
+ chart.Labels = []module.Label{
111
+ {Key: "iface", Value: iface},
112
+ }
113
+ for _, dim := range chart.Dims {
114
+ dim.ID = fmt.Sprintf(dim.ID, iface)
115
+ }
116
+ }
117
+
118
+ if err := c.Charts().Add(*charts...); err != nil {
119
+ c.Warningf("failed to add chart for interfce '%s': %v", iface, err)
120
+ }
121
+}
src/go/plugin/go.d/collector/ethtool/collect.go
new
+24
@@ -0,0 +1,24 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "strings"
7
+)
8
+
9
+func (c *Collector) collect() (map[string]int64, error) {
10
+ mx := make(map[string]int64)
11
+
12
+ for _, iface := range strings.Fields(c.OpticInterfaces) {
13
+ if c.ignoredOpticIfaces[iface] {
14
+ continue
15
+ }
16
+
17
+ if err := c.collectModuleEeprom(mx, iface); err != nil {
18
+ c.ignoredOpticIfaces[iface] = true
19
+ c.Errorf("failed to collect ddm info for %s: %v", iface, err)
20
+ }
21
+ }
22
+
23
+ return mx, nil
24
+}
src/go/plugin/go.d/collector/ethtool/collect_module_eeprom.go
new
+147
@@ -0,0 +1,147 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+)
13
+
14
+const precision = 1000
15
+
16
+type (
17
+ moduleEeprom struct {
18
+ ddm *moduleDdm
19
+ }
20
+ moduleDdm struct {
21
+ laserBiasMA *float64
22
+ laserPowerMW *float64
23
+ laserPowerDBM *float64
24
+ rxSignalPowerMW *float64
25
+ rxSignalPowerDBM *float64
26
+ tempC *float64
27
+ tempF *float64
28
+ voltageV *float64
29
+ }
30
+)
31
+
32
+func (c *Collector) collectModuleEeprom(mx map[string]int64, iface string) error {
33
+ bs, err := c.exec.moduleEeprom(iface)
34
+ if err != nil {
35
+ return fmt.Errorf("failed to get eeprom: %w", err)
36
+ }
37
+
38
+ eeprom, err := parseEeprom(bs)
39
+ if err != nil {
40
+ return fmt.Errorf("failed to parse eeprom: %w", err)
41
+ }
42
+ if eeprom == nil || eeprom.ddm == nil {
43
+ return errors.New("module doesn't have ddm")
44
+ }
45
+
46
+ if !c.seenOpticIfaces[iface] {
47
+ c.seenOpticIfaces[iface] = true
48
+ c.addModuleEepromCharts(iface, eeprom)
49
+ }
50
+
51
+ px := fmt.Sprintf("iface_%s_", iface)
52
+
53
+ writeDdmValue(mx, px+"laser_bias_current_ma", eeprom.ddm.laserBiasMA)
54
+ writeDdmValue(mx, px+"laser_output_power_mw", eeprom.ddm.laserPowerMW)
55
+ writeDdmValue(mx, px+"laser_output_power_dbm", eeprom.ddm.laserPowerDBM)
56
+ writeDdmValue(mx, px+"receiver_signal_average_optical_power_mw", eeprom.ddm.rxSignalPowerMW)
57
+ writeDdmValue(mx, px+"receiver_signal_average_optical_power_dbm", eeprom.ddm.rxSignalPowerDBM)
58
+ writeDdmValue(mx, px+"module_temperature_c", eeprom.ddm.tempC)
59
+ writeDdmValue(mx, px+"module_temperature_f", eeprom.ddm.tempF)
60
+ writeDdmValue(mx, px+"module_voltage_v", eeprom.ddm.voltageV)
61
+
62
+ return nil
63
+}
64
+
65
+func parseEeprom(bs []byte) (*moduleEeprom, error) {
66
+ var ddm moduleDdm
67
+ var foundDdm bool
68
+
69
+ sc := bufio.NewScanner(bytes.NewReader(bs))
70
+
71
+ for sc.Scan() {
72
+ line := strings.TrimSpace(sc.Text())
73
+ if line == "" {
74
+ continue
75
+ }
76
+
77
+ metric, value, ok := cutTrimSpace(line, ":")
78
+ if !ok {
79
+ continue
80
+ }
81
+
82
+ var err error
83
+
84
+ switch metric {
85
+ case "Laser bias current":
86
+ err = parseDdmValue(&ddm.laserBiasMA, value)
87
+ case "Laser output power":
88
+ err = parseCompoundDdmValues(&ddm.laserPowerMW, &ddm.laserPowerDBM, value)
89
+ case "Receiver signal average optical power":
90
+ err = parseCompoundDdmValues(&ddm.rxSignalPowerMW, &ddm.rxSignalPowerDBM, value)
91
+ case "Module temperature":
92
+ err = parseCompoundDdmValues(&ddm.tempC, &ddm.tempF, value)
93
+ case "Module voltage":
94
+ err = parseDdmValue(&ddm.voltageV, value)
95
+ default:
96
+ continue
97
+ }
98
+ if err != nil {
99
+ return nil, fmt.Errorf("failed to parse '%s': %v", line, err)
100
+ }
101
+ foundDdm = true
102
+ }
103
+
104
+ if !foundDdm {
105
+ return nil, nil
106
+ }
107
+ return &moduleEeprom{ddm: &ddm}, nil
108
+}
109
+
110
+func writeDdmValue(mx map[string]int64, key string, v *float64) {
111
+ if v == nil {
112
+ return
113
+ }
114
+ mx[key] = int64(*v * precision)
115
+}
116
+
117
+func parseDdmValue(v **float64, s string) error {
118
+ val, _, ok := cutTrimSpace(s, " ")
119
+ if !ok {
120
+ return errors.New("missing value")
121
+ }
122
+ f, err := strconv.ParseFloat(val, 64)
123
+ if err != nil {
124
+ return fmt.Errorf("invalid number '%s': %v", val, err)
125
+ }
126
+ *v = &f
127
+ return nil
128
+}
129
+
130
+func parseCompoundDdmValues(v1, v2 **float64, s string) error {
131
+ val1, val2, ok := cutTrimSpace(s, "/")
132
+ if !ok {
133
+ return errors.New("missing compound values")
134
+ }
135
+ if err := parseDdmValue(v1, val1); err != nil {
136
+ return err
137
+ }
138
+ if err := parseDdmValue(v2, val2); err != nil {
139
+ return err
140
+ }
141
+ return nil
142
+}
143
+
144
+func cutTrimSpace(s string, sep string) (string, string, bool) {
145
+ b, a, ok := strings.Cut(s, sep)
146
+ return strings.TrimSpace(b), strings.TrimSpace(a), ok
147
+}
src/go/plugin/go.d/collector/ethtool/collector.go
new
+109
@@ -0,0 +1,109 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "context"
7
+ _ "embed"
8
+ "errors"
9
+ "fmt"
10
+ "time"
11
+
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
14
+)
15
+
16
+//go:embed "config_schema.json"
17
+var configSchema string
18
+
19
+func init() {
20
+ module.Register("ethtool", module.Creator{
21
+ JobConfigSchema: configSchema,
22
+ Defaults: module.Defaults{
23
+ UpdateEvery: 10,
24
+ },
25
+ Create: func() module.Module { return New() },
26
+ Config: func() any { return &Config{} },
27
+ })
28
+}
29
+
30
+func New() *Collector {
31
+ return &Collector{
32
+ Config: Config{
33
+ BinaryPath: "/usr/sbin/ethtool",
34
+ Timeout: confopt.Duration(time.Second * 2),
35
+ },
36
+ charts: &module.Charts{},
37
+ seenOpticIfaces: make(map[string]bool),
38
+ ignoredOpticIfaces: make(map[string]bool),
39
+ }
40
+}
41
+
42
+type Config struct {
43
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
44
+ Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
45
+ BinaryPath string `yaml:"binary_path,omitempty" json:"binary_path"`
46
+ OpticInterfaces string `yaml:"optical_interfaces,omitempty" json:"optical_interfaces"`
47
+}
48
+
49
+type Collector struct {
50
+ module.Base
51
+ Config `yaml:",inline" json:""`
52
+
53
+ charts *module.Charts
54
+
55
+ exec ethtoolCli
56
+
57
+ seenOpticIfaces map[string]bool
58
+ ignoredOpticIfaces map[string]bool
59
+}
60
+
61
+func (c *Collector) Configuration() any {
62
+ return c.Config
63
+}
64
+
65
+func (c *Collector) Init(context.Context) error {
66
+ if err := c.validateConfig(); err != nil {
67
+ return fmt.Errorf("config validation: %s", err)
68
+ }
69
+
70
+ et, err := c.initEthtoolCli()
71
+ if err != nil {
72
+ return fmt.Errorf("ethtool exec initialization: %v", err)
73
+ }
74
+ c.exec = et
75
+
76
+ return nil
77
+}
78
+
79
+func (c *Collector) Check(context.Context) error {
80
+ mx, err := c.collect()
81
+ if err != nil {
82
+ return err
83
+ }
84
+
85
+ if len(mx) == 0 {
86
+ return errors.New("no metrics collected")
87
+ }
88
+
89
+ return nil
90
+}
91
+
92
+func (c *Collector) Charts() *module.Charts {
93
+ return c.charts
94
+}
95
+
96
+func (c *Collector) Collect(context.Context) map[string]int64 {
97
+ mx, err := c.collect()
98
+ if err != nil {
99
+ c.Error(err)
100
+ }
101
+
102
+ if len(mx) == 0 {
103
+ return nil
104
+ }
105
+
106
+ return mx
107
+}
108
+
109
+func (c *Collector) Cleanup(context.Context) {}
src/go/plugin/go.d/collector/ethtool/collector_test.go
new
+288
@@ -0,0 +1,288 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "context"
7
+ "errors"
8
+ "os"
9
+ "testing"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
12
+
13
+ "github.com/stretchr/testify/assert"
14
+ "github.com/stretchr/testify/require"
15
+)
16
+
17
+var (
18
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
19
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
20
+
21
+ dataModuleDdm, _ = os.ReadFile("testdata/ddm.txt")
22
+ dataModuleDdmNoFiber, _ = os.ReadFile("testdata/ddm-no-fiber.txt")
23
+ dataModuleNoDdm, _ = os.ReadFile("testdata/no-ddm.txt")
24
+)
25
+
26
+func Test_testDataIsValid(t *testing.T) {
27
+ for name, data := range map[string][]byte{
28
+ "dataConfigJSON": dataConfigJSON,
29
+ "dataConfigYAML": dataConfigYAML,
30
+
31
+ "dataModuleDdm": dataModuleDdm,
32
+ "dataModuleDdmNoFiber": dataModuleDdmNoFiber,
33
+ "dataModuleNoDdm": dataModuleNoDdm,
34
+ } {
35
+ require.NotNil(t, data, name)
36
+
37
+ }
38
+}
39
+
40
+func TestCollector_Configuration(t *testing.T) {
41
+ module.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
42
+}
43
+
44
+func TestCollector_Init(t *testing.T) {
45
+ tests := map[string]struct {
46
+ config Config
47
+ wantFail bool
48
+ }{
49
+ "fails if 'binary_path' is not set": {
50
+ wantFail: true,
51
+ config: Config{
52
+ BinaryPath: "",
53
+ },
54
+ },
55
+ "fails if failed to find binary": {
56
+ wantFail: true,
57
+ config: Config{
58
+ BinaryPath: "ethtool!!!",
59
+ },
60
+ },
61
+ }
62
+
63
+ for name, test := range tests {
64
+ t.Run(name, func(t *testing.T) {
65
+ collr := New()
66
+ collr.Config = test.config
67
+
68
+ if test.wantFail {
69
+ assert.Error(t, collr.Init(context.Background()))
70
+ } else {
71
+ assert.NoError(t, collr.Init(context.Background()))
72
+ }
73
+ })
74
+ }
75
+}
76
+
77
+func TestCollector_Cleanup(t *testing.T) {
78
+ tests := map[string]struct {
79
+ prepare func() *Collector
80
+ }{
81
+ "not initialized exec": {
82
+ prepare: func() *Collector {
83
+ return New()
84
+ },
85
+ },
86
+ "after check": {
87
+ prepare: func() *Collector {
88
+ collr := New()
89
+ collr.exec = prepareMockEepromDdm()
90
+ _ = collr.Check(context.Background())
91
+ return collr
92
+ },
93
+ },
94
+ "after collect": {
95
+ prepare: func() *Collector {
96
+ collr := New()
97
+ collr.exec = prepareMockEepromDdm()
98
+ _ = collr.Collect(context.Background())
99
+ return collr
100
+ },
101
+ },
102
+ }
103
+
104
+ for name, test := range tests {
105
+ t.Run(name, func(t *testing.T) {
106
+ collr := test.prepare()
107
+
108
+ assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
109
+ })
110
+ }
111
+}
112
+
113
+func TestCollector_Charts(t *testing.T) {
114
+ assert.NotNil(t, New().Charts())
115
+}
116
+
117
+func TestCollector_Check(t *testing.T) {
118
+ tests := map[string]struct {
119
+ prepareMock func() *mockEthtoolExec
120
+ wantFail bool
121
+ }{
122
+ "module with ddm": {
123
+ wantFail: false,
124
+ prepareMock: prepareMockEepromDdm,
125
+ },
126
+ "module with ddm no fiber": {
127
+ wantFail: false,
128
+ prepareMock: prepareMockEepromDdmNoFiber,
129
+ },
130
+ "module without ddm": {
131
+ wantFail: true,
132
+ prepareMock: prepareMockEepromNoDdm,
133
+ },
134
+ "moduleEeprom() error": {
135
+ wantFail: true,
136
+ prepareMock: prepareMockEepromError,
137
+ },
138
+ "moduleEeprom() unexpected response": {
139
+ wantFail: true,
140
+ prepareMock: prepareMockEepromUnexpectedResponse,
141
+ },
142
+ }
143
+
144
+ for name, test := range tests {
145
+ t.Run(name, func(t *testing.T) {
146
+ collr := New()
147
+ mock := test.prepareMock()
148
+ collr.exec = mock
149
+ collr.OpticInterfaces = "eth1 eth2"
150
+
151
+ if test.wantFail {
152
+ assert.Error(t, collr.Check(context.Background()))
153
+ } else {
154
+ assert.NoError(t, collr.Check(context.Background()))
155
+ }
156
+ })
157
+ }
158
+}
159
+
160
+func TestCollector_Collect(t *testing.T) {
161
+ tests := map[string]struct {
162
+ prepareMock func() *mockEthtoolExec
163
+ wantMetrics map[string]int64
164
+ wantCharts int
165
+ }{
166
+ "module with ddm": {
167
+ prepareMock: prepareMockEepromDdm,
168
+ wantCharts: len(ifaceModuleEepromCharts) * 2,
169
+ wantMetrics: map[string]int64{
170
+ "iface_eth1_laser_bias_current_ma": 13088,
171
+ "iface_eth1_laser_output_power_dbm": 5580,
172
+ "iface_eth1_laser_output_power_mw": 3611,
173
+ "iface_eth1_module_temperature_c": 38000,
174
+ "iface_eth1_module_temperature_f": 100400,
175
+ "iface_eth1_module_voltage_v": 3484,
176
+ "iface_eth1_receiver_signal_average_optical_power_dbm": -23570,
177
+ "iface_eth1_receiver_signal_average_optical_power_mw": 4,
178
+ "iface_eth2_laser_bias_current_ma": 13088,
179
+ "iface_eth2_laser_output_power_dbm": 5580,
180
+ "iface_eth2_laser_output_power_mw": 3611,
181
+ "iface_eth2_module_temperature_c": 38000,
182
+ "iface_eth2_module_temperature_f": 100400,
183
+ "iface_eth2_module_voltage_v": 3484,
184
+ "iface_eth2_receiver_signal_average_optical_power_dbm": -23570,
185
+ "iface_eth2_receiver_signal_average_optical_power_mw": 4,
186
+ },
187
+ },
188
+ "module with ddm no fiber": {
189
+ prepareMock: prepareMockEepromDdmNoFiber,
190
+ wantCharts: len(ifaceModuleEepromCharts) * 2,
191
+ wantMetrics: map[string]int64{
192
+ "iface_eth1_laser_bias_current_ma": 12768,
193
+ "iface_eth1_laser_output_power_dbm": 4840,
194
+ "iface_eth1_laser_output_power_mw": 3048,
195
+ "iface_eth1_module_temperature_c": 36750,
196
+ "iface_eth1_module_temperature_f": 98150,
197
+ "iface_eth1_module_voltage_v": 3486,
198
+ "iface_eth1_receiver_signal_average_optical_power_dbm": -40000,
199
+ "iface_eth1_receiver_signal_average_optical_power_mw": 0,
200
+ "iface_eth2_laser_bias_current_ma": 12768,
201
+ "iface_eth2_laser_output_power_dbm": 4840,
202
+ "iface_eth2_laser_output_power_mw": 3048,
203
+ "iface_eth2_module_temperature_c": 36750,
204
+ "iface_eth2_module_temperature_f": 98150,
205
+ "iface_eth2_module_voltage_v": 3486,
206
+ "iface_eth2_receiver_signal_average_optical_power_dbm": -40000,
207
+ "iface_eth2_receiver_signal_average_optical_power_mw": 0,
208
+ },
209
+ },
210
+ "module without ddm": {
211
+ prepareMock: prepareMockEepromNoDdm,
212
+ wantMetrics: nil,
213
+ },
214
+ "moduleEeprom() error": {
215
+ prepareMock: prepareMockEepromError,
216
+ wantMetrics: nil,
217
+ },
218
+ "moduleEeprom() unexpected response": {
219
+ prepareMock: prepareMockEepromUnexpectedResponse,
220
+ wantMetrics: nil,
221
+ },
222
+ }
223
+
224
+ for name, test := range tests {
225
+ t.Run(name, func(t *testing.T) {
226
+ collr := New()
227
+ mock := test.prepareMock()
228
+ collr.exec = mock
229
+ collr.OpticInterfaces = "eth1 eth2"
230
+
231
+ mx := collr.Collect(context.Background())
232
+
233
+ require.Equal(t, test.wantMetrics, mx)
234
+
235
+ assert.Equal(t, test.wantCharts, test.wantCharts, "want charts")
236
+
237
+ if len(test.wantMetrics) > 0 {
238
+ module.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
239
+ }
240
+ })
241
+ }
242
+}
243
+
244
+func prepareMockEepromDdm() *mockEthtoolExec {
245
+ return &mockEthtoolExec{
246
+ moduleEepromData: dataModuleDdm,
247
+ }
248
+}
249
+
250
+func prepareMockEepromDdmNoFiber() *mockEthtoolExec {
251
+ return &mockEthtoolExec{
252
+ moduleEepromData: dataModuleDdmNoFiber,
253
+ }
254
+}
255
+
256
+func prepareMockEepromNoDdm() *mockEthtoolExec {
257
+ return &mockEthtoolExec{
258
+ moduleEepromData: dataModuleNoDdm,
259
+ }
260
+}
261
+
262
+func prepareMockEepromError() *mockEthtoolExec {
263
+ return &mockEthtoolExec{
264
+ errOnModuleEeprom: true,
265
+ }
266
+}
267
+
268
+func prepareMockEepromUnexpectedResponse() *mockEthtoolExec {
269
+ return &mockEthtoolExec{
270
+ moduleEepromData: []byte(`
271
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
272
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
273
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
274
+`),
275
+ }
276
+}
277
+
278
+type mockEthtoolExec struct {
279
+ errOnModuleEeprom bool
280
+ moduleEepromData []byte
281
+}
282
+
283
+func (m *mockEthtoolExec) moduleEeprom(_ string) ([]byte, error) {
284
+ if m.errOnModuleEeprom {
285
+ return nil, errors.New("mock.moduleEeprom() error")
286
+ }
287
+ return m.moduleEepromData, nil
288
+}
src/go/plugin/go.d/collector/ethtool/config_schema.json
new
+51
@@ -0,0 +1,51 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Ethtool collector configuration",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 10
13
+ },
14
+ "binary_path": {
15
+ "title": "Binary path",
16
+ "description": "Path to the `ethtool` binary.",
17
+ "type": "string",
18
+ "default": "/usr/sbin/ethtool"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for executing the binary, specified in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 2
26
+ },
27
+ "optical_interfaces": {
28
+ "title": "Optical Interfaces",
29
+ "description": "Space-separated list of optical interface names which must have optical transceiver modules with [DDM](https://en.wikipedia.org/wiki/Small_Form-factor_Pluggable#Digital_diagnostics_monitoring).",
30
+ "type": "string"
31
+ },
32
+ "required": [
33
+ "binary_path"
34
+ ]
35
+ }
36
+ },
37
+ "uiSchema": {
38
+ "uiOptions": {
39
+ "fullPage": true
40
+ },
41
+ "optical_interfaces": {
42
+ "ui:placeholder": "enp1s0 enp1s1 enp2s0"
43
+ },
44
+ "binary_path": {
45
+ "ui:help": "If an absolute path is provided, the collector will use it directly; otherwise, it will search for the binary in directories specified in the PATH environment variable."
46
+ },
47
+ "timeout": {
48
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
49
+ }
50
+ }
51
+}
src/go/plugin/go.d/collector/ethtool/exec.go
new
+47
@@ -0,0 +1,47 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "context"
7
+ "fmt"
8
+ "os/exec"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/netdata/netdata/go/plugins/logger"
13
+)
14
+
15
+type ethtoolCli interface {
16
+ moduleEeprom(iface string) ([]byte, error)
17
+}
18
+
19
+func newEthtoolExec(binPath string, timeout time.Duration) *ethtoolCLIExec {
20
+ return ðtoolCLIExec{
21
+ binPath: binPath,
22
+ timeout: timeout,
23
+ }
24
+}
25
+
26
+type ethtoolCLIExec struct {
27
+ *logger.Logger
28
+
29
+ binPath string
30
+ timeout time.Duration
31
+}
32
+
33
+func (e *ethtoolCLIExec) moduleEeprom(iface string) ([]byte, error) {
34
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35
+ defer cancel()
36
+
37
+ cmd := exec.CommandContext(ctx, e.binPath, "-m", iface)
38
+ e.Debugf("executing '%s'", cmd)
39
+
40
+ bs, err := cmd.Output()
41
+ if err != nil {
42
+ out := strings.ReplaceAll(string(bs), "\n", " ")
43
+ return nil, fmt.Errorf("error on '%s': %v (%s)", cmd, err, out)
44
+ }
45
+
46
+ return bs, nil
47
+}
src/go/plugin/go.d/collector/ethtool/init.go
new
+41
@@ -0,0 +1,41 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ethtool
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+)
11
+
12
+func (c *Collector) validateConfig() error {
13
+ if c.OpticInterfaces == "" {
14
+ return errors.New("no optic interfaces specified")
15
+ }
16
+ if c.BinaryPath == "" {
17
+ return errors.New("no ethtool binary path specified")
18
+ }
19
+ return nil
20
+}
21
+
22
+func (c *Collector) initEthtoolCli() (ethtoolCli, error) {
23
+ binPath := c.BinaryPath
24
+
25
+ if !strings.HasPrefix(binPath, "/") {
26
+ path, err := exec.LookPath(binPath)
27
+ if err != nil {
28
+ return nil, err
29
+ }
30
+ binPath = path
31
+ }
32
+
33
+ if _, err := os.Stat(binPath); err != nil {
34
+ return nil, err
35
+ }
36
+
37
+ et := newEthtoolExec(binPath, c.Timeout.Duration())
38
+ et.Logger = c.Logger
39
+
40
+ return et, nil
41
+}
src/go/plugin/go.d/collector/ethtool/metadata.yaml
new
+131
@@ -0,0 +1,131 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-ethtool
5
+ plugin_name: go.d.plugin
6
+ module_name: ethtool
7
+ monitored_instance:
8
+ name: Network interfaces (hardware)
9
+ link: ""
10
+ icon_filename: network-wired.svg
11
+ categories:
12
+ - data-collection.data-collection.networking-stack-and-network-interfaces
13
+ keywords:
14
+ - sfp
15
+ - ddm
16
+ - optic
17
+ related_resources:
18
+ integrations:
19
+ list: []
20
+ info_provided_to_referring_integrations:
21
+ description: ""
22
+ most_popular: false
23
+ overview:
24
+ data_collection:
25
+ metrics_description: >
26
+ This collector monitors optical transceiver modules' diagnostic parameters
27
+ (temperature, voltage, laser bias current, transmit/receive power levels) from network interfaces
28
+ equipped with modules that support Digital Diagnostic Monitoring (DDM)
29
+ using the command line tool [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html).
30
+ method_description: ""
31
+ supported_platforms:
32
+ include: [Linux]
33
+ exclude: []
34
+ multi_instance: false
35
+ additional_permissions:
36
+ description: ""
37
+ default_behavior:
38
+ auto_detection:
39
+ description: ""
40
+ limits:
41
+ description: ""
42
+ performance_impact:
43
+ description: ""
44
+ setup:
45
+ prerequisites:
46
+ list: []
47
+ configuration:
48
+ file:
49
+ name: go.d/ethtool.conf
50
+ options:
51
+ description: |
52
+ The following options can be defined globally: update_every.
53
+ folding:
54
+ title: Config options
55
+ enabled: true
56
+ list:
57
+ - name: update_every
58
+ description: Data collection frequency.
59
+ default_value: 10
60
+ required: false
61
+ - name: binary_path
62
+ description: Path to the `ethtool` binary. If an absolute path is provided, the collector will use it directly; otherwise, it will search for the binary in directories specified in the PATH environment variable.
63
+ default_value: /usr/sbin/ethtool
64
+ required: true
65
+ - name: timeout
66
+ description: Timeout for executing the binary, specified in seconds.
67
+ default_value: 2
68
+ required: false
69
+ - name: optical_interfaces
70
+ description: Space-separated list of optical interface names which must have optical transceiver modules with [DDM](https://en.wikipedia.org/wiki/Small_Form-factor_Pluggable#Digital_diagnostics_monitoring).
71
+ default_value: ""
72
+ required: true
73
+ examples:
74
+ folding:
75
+ title: Config
76
+ enabled: true
77
+ list:
78
+ - name: Custom binary path
79
+ description: The executable is not in the directories specified in the PATH environment variable.
80
+ config: |
81
+ jobs:
82
+ - name: ethtool
83
+ binary_path: /usr/local/sbin/ethtool
84
+ optical_interfaces: "enp1s0 enp1s1 enp2s0"
85
+ troubleshooting:
86
+ problems:
87
+ list: []
88
+ alerts: []
89
+ metrics:
90
+ folding:
91
+ title: Metrics
92
+ enabled: false
93
+ description: ""
94
+ availability: []
95
+ scopes:
96
+ - name: Optical Transceiver Module
97
+ description: Metrics collected from optical transceiver modules that support Digital Diagnostic Monitoring (DDM).
98
+ labels:
99
+ - name: iface
100
+ description: Network interface name where the optical transceiver module is installed.
101
+ metrics:
102
+ - name: ethtool.optical_module_receiver_signal_power
103
+ description: Module Receiver Signal Average Optical Power
104
+ unit: 'dBm'
105
+ chart_type: line
106
+ dimensions:
107
+ - name: rx_power
108
+ - name: ethtool.optical_module_laser_output_power
109
+ description: Module Laser Output Power
110
+ unit: 'dBm'
111
+ chart_type: line
112
+ dimensions:
113
+ - name: tx_power
114
+ - name: ethtool.optical_module_laser_bias_current
115
+ description: Module Laser Bias Current
116
+ unit: 'mA'
117
+ chart_type: line
118
+ dimensions:
119
+ - name: bias_current
120
+ - name: ethtool.optical_module_temperature
121
+ description: Module Temperature
122
+ unit: 'Celsius'
123
+ chart_type: line
124
+ dimensions:
125
+ - name: temperature
126
+ - name: ethtool.optical_module_voltage
127
+ description: Module Voltage
128
+ unit: 'Volts'
129
+ chart_type: line
130
+ dimensions:
131
+ - name: voltage
src/go/plugin/go.d/collector/ethtool/testdata/config.json
new
+6
@@ -0,0 +1,6 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123,
4
+ "binary_path": "ok",
5
+ "optical_interfaces": "ok"
6
+}
src/go/plugin/go.d/collector/ethtool/testdata/config.yaml
new
+4
@@ -0,0 +1,4 @@
1
+update_every: 123
2
+timeout: 123.123
3
+binary_path: "ok"
4
+optical_interfaces: "ok"
src/go/plugin/go.d/collector/ethtool/testdata/ddm-no-fiber.txt
new
+74
@@ -0,0 +1,74 @@
1
+Identifier : 0x03 (SFP)
2
+Extended identifier : 0x04 (GBIC/SFP defined by 2-wire interface ID)
3
+Connector : 0x01 (SC)
4
+Transceiver codes : 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
5
+Transceiver type : 10G Ethernet: 10G Base-LR
6
+Encoding : 0x03 (NRZ)
7
+BR, Nominal : 10000MBd
8
+Rate identifier : 0x00 (unspecified)
9
+Length (SMF,km) : 20km
10
+Length (SMF) : 20000m
11
+Length (50um) : 0m
12
+Length (62.5um) : 0m
13
+Length (Copper) : 0m
14
+Length (OM3) : 0m
15
+Laser wavelength : 1270nm
16
+Vendor name : FS
17
+Vendor OUI : XXXXXXXX
18
+Vendor PN : XXXXXXXXXXXXXXX
19
+Vendor rev : XXXX
20
+Option values : 0x00 0x1a
21
+Option : RX_LOS implemented
22
+Option : TX_FAULT implemented
23
+Option : TX_DISABLE implemented
24
+BR margin, max : 0%
25
+BR margin, min : 0%
26
+Vendor SN : XXXXXXXXXXX
27
+Date code : XXXXXX
28
+Optical diagnostics support : Yes
29
+Laser bias current : 12.768 mA
30
+Laser output power : 3.0487 mW / 4.84 dBm
31
+Receiver signal average optical power : 0.0001 mW / -40.00 dBm
32
+Module temperature : 36.75 degrees C / 98.15 degrees F
33
+Module voltage : 3.4860 V
34
+Alarm/warning flags implemented : Yes
35
+Laser bias current high alarm : Off
36
+Laser bias current low alarm : Off
37
+Laser bias current high warning : Off
38
+Laser bias current low warning : Off
39
+Laser output power high alarm : Off
40
+Laser output power low alarm : Off
41
+Laser output power high warning : Off
42
+Laser output power low warning : Off
43
+Module temperature high alarm : Off
44
+Module temperature low alarm : Off
45
+Module temperature high warning : Off
46
+Module temperature low warning : Off
47
+Module voltage high alarm : Off
48
+Module voltage low alarm : Off
49
+Module voltage high warning : Off
50
+Module voltage low warning : Off
51
+Laser rx power high alarm : Off
52
+Laser rx power low alarm : On
53
+Laser rx power high warning : Off
54
+Laser rx power low warning : On
55
+Laser bias current high alarm threshold : 95.000 mA
56
+Laser bias current low alarm threshold : 1.000 mA
57
+Laser bias current high warning threshold : 85.000 mA
58
+Laser bias current low warning threshold : 2.000 mA
59
+Laser output power high alarm threshold : 6.4566 mW / 8.10 dBm
60
+Laser output power low alarm threshold : 1.9953 mW / 3.00 dBm
61
+Laser output power high warning threshold : 6.4566 mW / 8.10 dBm
62
+Laser output power low warning threshold : 2.5119 mW / 4.00 dBm
63
+Module temperature high alarm threshold : 100.00 degrees C / 212.00 degrees F
64
+Module temperature low alarm threshold : -40.00 degrees C / -40.00 degrees F
65
+Module temperature high warning threshold : 90.00 degrees C / 194.00 degrees F
66
+Module temperature low warning threshold : -30.00 degrees C / -22.00 degrees F
67
+Module voltage high alarm threshold : 3.7000 V
68
+Module voltage low alarm threshold : 2.8999 V
69
+Module voltage high warning threshold : 3.5000 V
70
+Module voltage low warning threshold : 3.1000 V
71
+Laser rx power high alarm threshold : 0.1995 mW / -7.00 dBm
72
+Laser rx power low alarm threshold : 0.0010 mW / -30.00 dBm
73
+Laser rx power high warning threshold : 0.1584 mW / -8.00 dBm
74
+Laser rx power low warning threshold : 0.0012 mW / -29.21 dBm
src/go/plugin/go.d/collector/ethtool/testdata/ddm.txt
new
+74
@@ -0,0 +1,74 @@
1
+Identifier : 0x03 (SFP)
2
+Extended identifier : 0x04 (GBIC/SFP defined by 2-wire interface ID)
3
+Connector : 0x01 (SC)
4
+Transceiver codes : 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
5
+Transceiver type : 10G Ethernet: 10G Base-LR
6
+Encoding : 0x03 (NRZ)
7
+BR, Nominal : 10000MBd
8
+Rate identifier : 0x00 (unspecified)
9
+Length (SMF,km) : 20km
10
+Length (SMF) : 20000m
11
+Length (50um) : 0m
12
+Length (62.5um) : 0m
13
+Length (Copper) : 0m
14
+Length (OM3) : 0m
15
+Laser wavelength : 1270nm
16
+Vendor name : FS
17
+Vendor OUI : XXXXXXXX
18
+Vendor PN : XXXXXXXXXXXXXXX
19
+Vendor rev : XXXX
20
+Option values : 0x00 0x1a
21
+Option : RX_LOS implemented
22
+Option : TX_FAULT implemented
23
+Option : TX_DISABLE implemented
24
+BR margin, max : 0%
25
+BR margin, min : 0%
26
+Vendor SN : XXXXXXXXXXX
27
+Date code : XXXXXX
28
+Optical diagnostics support : Yes
29
+Laser bias current : 13.088 mA
30
+Laser output power : 3.6119 mW / 5.58 dBm
31
+Receiver signal average optical power : 0.0044 mW / -23.57 dBm
32
+Module temperature : 38.00 degrees C / 100.40 degrees F
33
+Module voltage : 3.4844 V
34
+Alarm/warning flags implemented : Yes
35
+Laser bias current high alarm : Off
36
+Laser bias current low alarm : Off
37
+Laser bias current high warning : Off
38
+Laser bias current low warning : Off
39
+Laser output power high alarm : Off
40
+Laser output power low alarm : Off
41
+Laser output power high warning : Off
42
+Laser output power low warning : Off
43
+Module temperature high alarm : Off
44
+Module temperature low alarm : Off
45
+Module temperature high warning : Off
46
+Module temperature low warning : Off
47
+Module voltage high alarm : Off
48
+Module voltage low alarm : Off
49
+Module voltage high warning : Off
50
+Module voltage low warning : Off
51
+Laser rx power high alarm : Off
52
+Laser rx power low alarm : Off
53
+Laser rx power high warning : Off
54
+Laser rx power low warning : Off
55
+Laser bias current high alarm threshold : 95.000 mA
56
+Laser bias current low alarm threshold : 1.000 mA
57
+Laser bias current high warning threshold : 85.000 mA
58
+Laser bias current low warning threshold : 2.000 mA
59
+Laser output power high alarm threshold : 6.4566 mW / 8.10 dBm
60
+Laser output power low alarm threshold : 1.9953 mW / 3.00 dBm
61
+Laser output power high warning threshold : 6.4566 mW / 8.10 dBm
62
+Laser output power low warning threshold : 2.5119 mW / 4.00 dBm
63
+Module temperature high alarm threshold : 100.00 degrees C / 212.00 degrees F
64
+Module temperature low alarm threshold : -40.00 degrees C / -40.00 degrees F
65
+Module temperature high warning threshold : 90.00 degrees C / 194.00 degrees F
66
+Module temperature low warning threshold : -30.00 degrees C / -22.00 degrees F
67
+Module voltage high alarm threshold : 3.7000 V
68
+Module voltage low alarm threshold : 2.8999 V
69
+Module voltage high warning threshold : 3.5000 V
70
+Module voltage low warning threshold : 3.1000 V
71
+Laser rx power high alarm threshold : 0.1995 mW / -7.00 dBm
72
+Laser rx power low alarm threshold : 0.0010 mW / -30.00 dBm
73
+Laser rx power high warning threshold : 0.1584 mW / -8.00 dBm
74
+Laser rx power low warning threshold : 0.0012 mW / -29.21 dBm
src/go/plugin/go.d/collector/ethtool/testdata/no-ddm.txt
new
+29
@@ -0,0 +1,29 @@
1
+Identifier : 0x03 (SFP)
2
+Extended identifier : 0x04 (GBIC/SFP defined by 2-wire interface ID)
3
+Connector : 0x07 (LC)
4
+Transceiver codes : 0x10 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00
5
+Transceiver type : 10G Ethernet: 10G Base-SR
6
+Transceiver type : Ethernet: 1000BASE-SX
7
+Encoding : 0x06 (64B/66B)
8
+BR, Nominal : 10300MBd
9
+Rate identifier : 0x02 (8/4/2G Rx Rate_Select only)
10
+Length (SMF,km) : 0km
11
+Length (SMF) : 0m
12
+Length (50um) : 80m
13
+Length (62.5um) : 30m
14
+Length (Copper) : 0m
15
+Length (OM3) : 300m
16
+Laser wavelength : 850nm
17
+Vendor name : Intel Corp
18
+Vendor OUI : 00:XX:XX
19
+Vendor PN : FTXXXXXXXXXXX
20
+Vendor rev : A
21
+Option values : 0x00 0x3a
22
+Option : RX_LOS implemented
23
+Option : TX_FAULT implemented
24
+Option : TX_DISABLE implemented
25
+Option : RATE_SELECT implemented
26
+BR margin, max : 0%
27
+BR margin, min : 0%
28
+Vendor SN : XXXXXXXX
29
+Date code : XXXXXX
src/go/plugin/go.d/collector/init.go
+1
@@ -31,6 +31,7 @@ import (
31
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/dovecot"
32
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/elasticsearch"
33
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/envoy"
34
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/ethtool"
35
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/exim"
36
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/fail2ban"
37
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/filecheck"
src/go/plugin/go.d/config/go.d.conf
+1
@@ -42,6 +42,7 @@ modules:
42
# dovecot: yes
43
# elasticsearch: yes
44
# envoy: yes
45
+# ethtool: yes
46
# exim: yes
47
# fail2ban: yes
48
# filecheck: yes
src/go/plugin/go.d/config/go.d/ethtool.conf
new
+7
@@ -0,0 +1,7 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/ethtool#readme
3
+
4
+jobs:
5
+ - name: ethtool
6
+ ## Space-separated list of optical interface names which must have optical transceiver modules with DDM
7
+ optical_interfaces: "enp1s0 enp1s1 enp2s0"