@cryptotaxi247 / netdata-1 / commits / c8b3dd3f1

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 &ethtoolCLIExec{
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"