go.d/sensors: add sysfs scan method to collect metrics (#18431)
Ilya Mashchenko committed
Aug 29, 2024 at 11:43 UTC
c3261e3b6a2e99424bf8c3bd9731f7d77d6d5972
19 files changed
+804
-377
src/go/plugin/go.d/modules/sensors/charts.go
+63
-2
@@ -7,6 +7,7 @@ import (
7
"strings"
8
9
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors/lmsensors"
11
)
12
13
const (
@@ -17,6 +18,7 @@ const (
18
prioSensorFan
19
prioSensorEnergy
20
prioSensorHumidity
21
+ prioSensorIntrusion
22
)
23
24
var sensorTemperatureChartTmpl = module.Chart{
@@ -110,10 +112,24 @@ var sensorHumidityChartTmpl = module.Chart{
112
},
113
}
114
113
-func (s *Sensors) addSensorChart(sn sensorStats) {
115
+var sensorIntrusionChartTmpl = module.Chart{
116
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_intrusion",
117
+ Title: "Sensor intrusion",
118
+ Units: "status",
119
+ Fam: "intrusion",
120
+ Ctx: "sensors.sensor_intrusion",
121
+ Type: module.Line,
122
+ Priority: prioSensorIntrusion,
123
+ Dims: module.Dims{
124
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s_alarm_off", Name: "alarm_off"},
125
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s_alarm_on", Name: "alarm_on"},
126
+ },
127
+}
128
+
129
+func (s *Sensors) addExecSensorChart(sn execSensor) {
130
var chart *module.Chart
131
116
- switch sensorType(sn) {
132
+ switch sn.sensorType() {
133
case sensorTypeTemp:
134
chart = sensorTemperatureChartTmpl.Copy()
135
case sensorTypeVoltage:
@@ -148,6 +164,51 @@ func (s *Sensors) addSensorChart(sn sensorStats) {
164
}
165
}
166
167
+func (s *Sensors) addSysfsSensorChart(devName string, sn lmsensors.Sensor) {
168
+ var chart *module.Chart
169
+ var feat, subfeat string
170
+ devName = snakeCase(devName)
171
+
172
+ switch v := sn.(type) {
173
+ case *lmsensors.TemperatureSensor:
174
+ chart = sensorTemperatureChartTmpl.Copy()
175
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_input"
176
+ case *lmsensors.VoltageSensor:
177
+ chart = sensorVoltageChartTmpl.Copy()
178
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_input"
179
+ case *lmsensors.CurrentSensor:
180
+ chart = sensorCurrentChartTmpl.Copy()
181
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_input"
182
+ case *lmsensors.PowerSensor:
183
+ chart = sensorPowerChartTmpl.Copy()
184
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_average"
185
+ case *lmsensors.FanSensor:
186
+ chart = sensorFanChartTmpl.Copy()
187
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_input"
188
+ case *lmsensors.IntrusionSensor:
189
+ chart = sensorIntrusionChartTmpl.Copy()
190
+ feat, subfeat = firstNotEmpty(v.Label, v.Name), v.Name+"_alarm"
191
+ default:
192
+ return
193
+ }
194
+
195
+ origFeat := feat
196
+ feat, subfeat = snakeCase(feat), snakeCase(subfeat)
197
+
198
+ chart.ID = fmt.Sprintf(chart.ID, devName, feat, subfeat)
199
+ chart.Labels = []module.Label{
200
+ {Key: "chip", Value: devName},
201
+ {Key: "feature", Value: origFeat},
202
+ }
203
+ for _, dim := range chart.Dims {
204
+ dim.ID = fmt.Sprintf(dim.ID, devName, feat, subfeat)
205
+ }
206
+
207
+ if err := s.Charts().Add(chart); err != nil {
208
+ s.Warning(err)
209
+ }
210
+}
211
+
212
func (s *Sensors) removeSensorChart(px string) {
213
for _, chart := range *s.Charts() {
214
if strings.HasPrefix(chart.ID, px) {
src/go/plugin/go.d/modules/sensors/collect.go
+3
-170
@@ -2,178 +2,11 @@
2
3
package sensors
4
5
-import (
6
- "bufio"
7
- "bytes"
8
- "errors"
9
- "fmt"
10
- "strconv"
11
- "strings"
12
-)
13
-
14
-type sensorStats struct {
15
- chip string
16
- feature string
17
- subfeature string
18
- value string
19
-}
20
-
21
-func (s *sensorStats) String() string {
22
- return fmt.Sprintf("chip:%s feat:%s subfeat:%s value:%s", s.chip, s.feature, s.subfeature, s.value)
23
-}
24
-
25
-const (
26
- sensorTypeTemp = "temperature"
27
- sensorTypeVoltage = "voltage"
28
- sensorTypePower = "power"
29
- sensorTypeHumidity = "humidity"
30
- sensorTypeFan = "fan"
31
- sensorTypeCurrent = "current"
32
- sensorTypeEnergy = "energy"
33
-)
34
-
5
const precision = 1000
6
7
func (s *Sensors) collect() (map[string]int64, error) {
38
- bs, err := s.exec.sensorsInfo()
39
- if err != nil {
40
- return nil, err
41
- }
42
-
43
- if len(bs) == 0 {
44
- return nil, errors.New("empty response from sensors")
45
- }
46
-
47
- sensors, err := parseSensors(bs)
48
- if err != nil {
49
- return nil, err
50
- }
51
- if len(sensors) == 0 {
52
- return nil, errors.New("no sensors found")
53
- }
54
-
55
- mx := make(map[string]int64)
56
- seen := make(map[string]bool)
57
-
58
- for _, sn := range sensors {
59
- // TODO: Most likely we need different values depending on the type of sensor.
60
- if !strings.HasSuffix(sn.subfeature, "_input") {
61
- s.Debugf("skipping non input sensor: '%s'", sn)
62
- continue
63
- }
64
-
65
- v, err := strconv.ParseFloat(sn.value, 64)
66
- if err != nil {
67
- s.Debugf("parsing value for sensor '%s': %v", sn, err)
68
- continue
69
- }
70
-
71
- if sensorType(sn) == "" {
72
- s.Debugf("can not find type for sensor '%s'", sn)
73
- continue
74
- }
75
-
76
- if minVal, maxVal, ok := sensorLimits(sn); ok && (v < minVal || v > maxVal) {
77
- s.Debugf("value outside limits [%d/%d] for sensor '%s'", int64(minVal), int64(maxVal), sn)
78
- continue
79
- }
80
-
81
- key := fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s", sn.chip, sn.feature, sn.subfeature)
82
- key = snakeCase(key)
83
- if !s.sensors[key] {
84
- s.sensors[key] = true
85
- s.addSensorChart(sn)
86
- }
87
-
88
- seen[key] = true
89
-
90
- mx[key] = int64(v * precision)
8
+ if s.exec != nil {
9
+ return s.collectExec()
10
}
92
-
93
- for k := range s.sensors {
94
- if !seen[k] {
95
- delete(s.sensors, k)
96
- s.removeSensorChart(k)
97
- }
98
- }
99
-
100
- return mx, nil
101
-}
102
-
103
-func snakeCase(n string) string {
104
- return strings.ToLower(strings.ReplaceAll(n, " ", "_"))
105
-}
106
-
107
-func sensorLimits(sn sensorStats) (minVal float64, maxVal float64, ok bool) {
108
- switch sensorType(sn) {
109
- case sensorTypeTemp:
110
- return -127, 1000, true
111
- case sensorTypeVoltage:
112
- return -400, 400, true
113
- case sensorTypeCurrent:
114
- return -127, 127, true
115
- case sensorTypeFan:
116
- return 0, 65535, true
117
- default:
118
- return 0, 0, false
119
- }
120
-}
121
-
122
-func sensorType(sn sensorStats) string {
123
- switch {
124
- case strings.HasPrefix(sn.subfeature, "temp"):
125
- return sensorTypeTemp
126
- case strings.HasPrefix(sn.subfeature, "in"):
127
- return sensorTypeVoltage
128
- case strings.HasPrefix(sn.subfeature, "power"):
129
- return sensorTypePower
130
- case strings.HasPrefix(sn.subfeature, "humidity"):
131
- return sensorTypeHumidity
132
- case strings.HasPrefix(sn.subfeature, "fan"):
133
- return sensorTypeFan
134
- case strings.HasPrefix(sn.subfeature, "curr"):
135
- return sensorTypeCurrent
136
- case strings.HasPrefix(sn.subfeature, "energy"):
137
- return sensorTypeEnergy
138
- default:
139
- return ""
140
- }
141
-}
142
-
143
-func parseSensors(output []byte) ([]sensorStats, error) {
144
- var sensors []sensorStats
145
-
146
- sc := bufio.NewScanner(bytes.NewReader(output))
147
-
148
- var chip, feat string
149
-
150
- for sc.Scan() {
151
- text := sc.Text()
152
- if text == "" {
153
- chip, feat = "", ""
154
- continue
155
- }
156
-
157
- switch {
158
- case strings.HasPrefix(text, " ") && chip != "" && feat != "":
159
- parts := strings.Split(text, ":")
160
- if len(parts) != 2 {
161
- continue
162
- }
163
- subfeat, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
164
- sensors = append(sensors, sensorStats{
165
- chip: chip,
166
- feature: feat,
167
- subfeature: subfeat,
168
- value: value,
169
- })
170
- case strings.HasSuffix(text, ":") && chip != "":
171
- feat = strings.TrimSpace(strings.TrimSuffix(text, ":"))
172
- default:
173
- chip = text
174
- feat = ""
175
- }
176
- }
177
-
178
- return sensors, nil
11
+ return s.collectSysfs()
12
}
src/go/plugin/go.d/modules/sensors/collect_exec.go
new
+188
@@ -0,0 +1,188 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+)
13
+
14
+const (
15
+ sensorTypeTemp = "temperature"
16
+ sensorTypeVoltage = "voltage"
17
+ sensorTypePower = "power"
18
+ sensorTypeHumidity = "humidity"
19
+ sensorTypeFan = "fan"
20
+ sensorTypeCurrent = "current"
21
+ sensorTypeEnergy = "energy"
22
+)
23
+
24
+type execSensor struct {
25
+ chip string
26
+ feature string
27
+ subfeature string
28
+ value string
29
+}
30
+
31
+func (s *execSensor) String() string {
32
+ return fmt.Sprintf("chip:%s feat:%s subfeat:%s value:%s", s.chip, s.feature, s.subfeature, s.value)
33
+}
34
+
35
+func (s *execSensor) sensorType() string {
36
+ switch {
37
+ case strings.HasPrefix(s.subfeature, "temp"):
38
+ return sensorTypeTemp
39
+ case strings.HasPrefix(s.subfeature, "in"):
40
+ return sensorTypeVoltage
41
+ case strings.HasPrefix(s.subfeature, "power"):
42
+ return sensorTypePower
43
+ case strings.HasPrefix(s.subfeature, "humidity"):
44
+ return sensorTypeHumidity
45
+ case strings.HasPrefix(s.subfeature, "fan"):
46
+ return sensorTypeFan
47
+ case strings.HasPrefix(s.subfeature, "curr"):
48
+ return sensorTypeCurrent
49
+ case strings.HasPrefix(s.subfeature, "energy"):
50
+ return sensorTypeEnergy
51
+ default:
52
+ return ""
53
+ }
54
+}
55
+
56
+func (s *execSensor) limits() (minVal float64, maxVal float64, ok bool) {
57
+ switch s.sensorType() {
58
+ case sensorTypeTemp:
59
+ return -127, 1000, true
60
+ case sensorTypeVoltage:
61
+ return -400, 400, true
62
+ case sensorTypeCurrent:
63
+ return -127, 127, true
64
+ case sensorTypeFan:
65
+ return 0, 65535, true
66
+ default:
67
+ return 0, 0, false
68
+ }
69
+}
70
+
71
+func (s *Sensors) collectExec() (map[string]int64, error) {
72
+ if s.exec == nil {
73
+ return nil, errors.New("exec sensor is not initialized")
74
+ }
75
+
76
+ s.Debugf("using sensors binary to collect metrics")
77
+
78
+ bs, err := s.exec.sensorsInfo()
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
+
83
+ if len(bs) == 0 {
84
+ return nil, errors.New("empty response from sensors")
85
+ }
86
+
87
+ sensors, err := parseExecSensors(bs)
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+ if len(sensors) == 0 {
92
+ return nil, errors.New("no sensors found")
93
+ }
94
+
95
+ mx := make(map[string]int64)
96
+ seen := make(map[string]bool)
97
+
98
+ for _, sn := range sensors {
99
+ sx := "_input"
100
+ if sn.sensorType() == sensorTypePower {
101
+ sx = "_average"
102
+ }
103
+
104
+ if !strings.HasSuffix(sn.subfeature, sx) {
105
+ s.Debugf("skipping sensor: '%s'", sn)
106
+ continue
107
+ }
108
+
109
+ v, err := strconv.ParseFloat(sn.value, 64)
110
+ if err != nil {
111
+ s.Debugf("parsing value for sensor '%s': %v", sn, err)
112
+ continue
113
+ }
114
+
115
+ if sn.sensorType() == "" {
116
+ s.Debugf("can not find type for sensor '%s'", sn)
117
+ continue
118
+ }
119
+
120
+ if minVal, maxVal, ok := sn.limits(); ok && (v < minVal || v > maxVal) {
121
+ s.Debugf("value outside limits [%d/%d] for sensor '%s'", int64(minVal), int64(maxVal), sn)
122
+ continue
123
+ }
124
+
125
+ key := fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s", sn.chip, sn.feature, sn.subfeature)
126
+ key = snakeCase(key)
127
+
128
+ if !s.sensors[key] {
129
+ s.sensors[key] = true
130
+ s.addExecSensorChart(sn)
131
+ }
132
+
133
+ seen[key] = true
134
+
135
+ mx[key] = int64(v * precision)
136
+ }
137
+
138
+ for k := range s.sensors {
139
+ if !seen[k] {
140
+ delete(s.sensors, k)
141
+ s.removeSensorChart(k)
142
+ }
143
+ }
144
+
145
+ return mx, nil
146
+}
147
+
148
+func snakeCase(n string) string {
149
+ return strings.ToLower(strings.ReplaceAll(n, " ", "_"))
150
+}
151
+
152
+func parseExecSensors(output []byte) ([]execSensor, error) {
153
+ var sensors []execSensor
154
+
155
+ sc := bufio.NewScanner(bytes.NewReader(output))
156
+
157
+ var chip, feat string
158
+
159
+ for sc.Scan() {
160
+ text := sc.Text()
161
+ if text == "" {
162
+ chip, feat = "", ""
163
+ continue
164
+ }
165
+
166
+ switch {
167
+ case strings.HasPrefix(text, " ") && chip != "" && feat != "":
168
+ parts := strings.Split(text, ":")
169
+ if len(parts) != 2 {
170
+ continue
171
+ }
172
+ subfeat, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
173
+ sensors = append(sensors, execSensor{
174
+ chip: chip,
175
+ feature: feat,
176
+ subfeature: subfeat,
177
+ value: value,
178
+ })
179
+ case strings.HasSuffix(text, ":") && chip != "":
180
+ feat = strings.TrimSpace(strings.TrimSuffix(text, ":"))
181
+ default:
182
+ chip = text
183
+ feat = ""
184
+ }
185
+ }
186
+
187
+ return sensors, nil
188
+}
src/go/plugin/go.d/modules/sensors/collect_sysfs.go
new
+97
@@ -0,0 +1,97 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+
9
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors/lmsensors"
10
+)
11
+
12
+func (s *Sensors) collectSysfs() (map[string]int64, error) {
13
+ if s.sc == nil {
14
+ return nil, errors.New("sysfs scanner is not initialized")
15
+ }
16
+
17
+ s.Debugf("using sysfs scan to collect metrics")
18
+
19
+ devices, err := s.sc.Scan()
20
+ if err != nil {
21
+ return nil, err
22
+ }
23
+
24
+ if len(devices) == 0 {
25
+ return nil, errors.New("sysfs scanner: devices found")
26
+ }
27
+
28
+ seen := make(map[string]bool)
29
+ mx := make(map[string]int64)
30
+
31
+ for _, dev := range devices {
32
+ for _, sn := range dev.Sensors {
33
+ var key string
34
+
35
+ switch v := sn.(type) {
36
+ case *lmsensors.TemperatureSensor:
37
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_input", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
38
+ mx[key] = int64(v.Input * precision)
39
+ case *lmsensors.VoltageSensor:
40
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_input", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
41
+ mx[key] = int64(v.Input * precision)
42
+ case *lmsensors.CurrentSensor:
43
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_input", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
44
+ mx[key] = int64(v.Input * precision)
45
+ case *lmsensors.PowerSensor:
46
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_average", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
47
+ mx[key] = int64(v.Average * precision)
48
+ case *lmsensors.FanSensor:
49
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_input", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
50
+ mx[key] = int64(v.Input * precision)
51
+ case *lmsensors.IntrusionSensor:
52
+ key = snakeCase(fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s_alarm", dev.Name, firstNotEmpty(v.Label, v.Name), v.Name))
53
+ mx[key+"_on"] = boolToInt(v.Alarm)
54
+ mx[key+"_off"] = boolToInt(!v.Alarm)
55
+ default:
56
+ s.Debugf("unexpected sensor type: %T", v)
57
+ continue
58
+ }
59
+
60
+ seen[key] = true
61
+
62
+ if !s.sensors[key] {
63
+ s.sensors[key] = true
64
+ s.addSysfsSensorChart(dev.Name, sn)
65
+ }
66
+ }
67
+ }
68
+
69
+ if len(mx) == 0 {
70
+ return nil, errors.New("sysfs scanner: no metrics collected")
71
+ }
72
+
73
+ for k := range s.sensors {
74
+ if !seen[k] {
75
+ delete(s.sensors, k)
76
+ s.removeSensorChart(k)
77
+ }
78
+ }
79
+
80
+ return mx, nil
81
+}
82
+
83
+func firstNotEmpty(s ...string) string {
84
+ for _, v := range s {
85
+ if v != "" {
86
+ return v
87
+ }
88
+ }
89
+ return ""
90
+}
91
+
92
+func boolToInt(b bool) int64 {
93
+ if b {
94
+ return 1
95
+ }
96
+ return 0
97
+}
src/go/plugin/go.d/modules/sensors/config_schema.json
+1
-2
@@ -13,7 +13,7 @@
13
},
14
"binary_path": {
15
"title": "Binary path",
16
- "description": "Path to the `sensors` binary.",
16
+ "description": "Path to the `sensors` binary. If left empty or if the binary is not found, [**sysfs**](https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface) will be used to collect sensor statistics.",
17
"type": "string",
18
"default": "/usr/bin/sensors"
19
},
@@ -26,7 +26,6 @@
26
}
27
},
28
"required": [
29
- "binary_path"
29
],
30
"additionalProperties": false,
31
"patternProperties": {
src/go/plugin/go.d/modules/sensors/init.go
+2
-6
@@ -3,20 +3,16 @@
3
package sensors
4
5
import (
6
- "errors"
6
"os"
7
"os/exec"
8
"strings"
9
)
10
12
-func (s *Sensors) validateConfig() error {
11
+func (s *Sensors) initSensorsBinary() (sensorsBinary, error) {
12
if s.BinaryPath == "" {
14
- return errors.New("no sensors binary path specified")
13
+ return nil, nil
14
}
16
- return nil
17
-}
15
19
-func (s *Sensors) initSensorsCliExec() (sensorsCLI, error) {
16
binPath := s.BinaryPath
17
18
if !strings.HasPrefix(binPath, "/") {
src/go/plugin/go.d/modules/sensors/lmsensors/README.md
+2
-3
@@ -1,5 +1,4 @@
1
-lmsensors [](https://travis-ci.org/mdlayher/lmsensors) [](http://godoc.org/github.com/mdlayher/lmsensors) [](https://goreportcard.com/report/github.com/mdlayher/lmsensors)
1
+lmsensors
2
=========
3
4
-Package `lmsensors` provides access to Linux monitoring sensors data, such
5
-as temperatures, voltage, and fan speeds. MIT Licensed.
4
+Modified version of [mdlayher/lmsensors](https://github.com/mdlayher/lmsensors).
\ No newline at end of file
src/go/plugin/go.d/modules/sensors/lmsensors/currentsensor.go
+5
-5
@@ -11,11 +11,10 @@ type CurrentSensor struct {
11
// The name of the sensor.
12
Name string
13
14
- // A label that describes what the sensor is monitoring. Label may be
15
- // empty.
14
+ // A label that describes what the sensor is monitoring. Label may be empty.
15
Label string
16
18
- // Whether or not the sensor has an alarm triggered.
17
+ // Whether the sensor has an alarm triggered.
18
Alarm bool
19
20
// The input current, in Amperes, indicated by the sensor.
@@ -28,8 +27,7 @@ type CurrentSensor struct {
27
Critical float64
28
}
29
31
-func (s *CurrentSensor) name() string { return s.Name }
32
-func (s *CurrentSensor) setName(name string) { s.Name = name }
30
+func (s *CurrentSensor) Type() SensorType { return SensorTypeCurrent }
31
32
func (s *CurrentSensor) parse(raw map[string]string) error {
33
for k, v := range raw {
@@ -60,3 +58,5 @@ func (s *CurrentSensor) parse(raw map[string]string) error {
58
59
return nil
60
}
61
+
62
+func (s *CurrentSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/lmsensors/fansensor.go
+22
-13
@@ -11,45 +11,54 @@ type FanSensor struct {
11
// The name of the sensor.
12
Name string
13
14
- // Whether or not the fan speed is below the minimum threshold.
14
+ // A label that describes what the sensor is monitoring. Label may be empty.
15
+ Label string
16
+
17
+ // Whether the fan speed is below the minimum threshold.
18
Alarm bool
19
17
- // Whether or not the fan will sound an audible alarm when fan speed is
18
- // below the minimum threshold.
20
+ // Whether the fan will sound an audible alarm when fan speed is below the minimum threshold.
21
Beep bool
22
23
// The input fan speed, in rotations per minute, indicated by the sensor.
22
- Input int
24
+ Input float64
25
+
26
+ // The low threshold fan speed, in rotations per minute, indicated by the sensor.
27
+ Minimum float64
28
24
- // The low threshold fan speed, in rotations per minute, indicated by the
25
- // sensor.
26
- Minimum int
29
+ // The high threshold fan speed, in rotations per minute, indicated by the sensor.
30
+ Maximum float64
31
}
32
29
-func (s *FanSensor) name() string { return s.Name }
30
-func (s *FanSensor) setName(name string) { s.Name = name }
33
+func (s *FanSensor) Type() SensorType { return SensorTypeFan }
34
35
func (s *FanSensor) parse(raw map[string]string) error {
36
for k, v := range raw {
37
switch k {
35
- case "input", "min":
36
- i, err := strconv.Atoi(v)
38
+ case "input", "min", "max":
39
+ f, err := strconv.ParseFloat(v, 64)
40
if err != nil {
41
return err
42
}
43
44
switch k {
45
case "input":
43
- s.Input = i
46
+ s.Input = f
47
case "min":
45
- s.Minimum = i
48
+ s.Minimum = f
49
+ case "max":
50
+ s.Maximum = f
51
}
52
case "alarm":
53
s.Alarm = v != "0"
54
case "beep":
55
s.Beep = v != "0"
56
+ case "label":
57
+ s.Label = v
58
}
59
}
60
61
return nil
62
}
63
+
64
+func (s *FanSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/lmsensors/intrusionsensor.go
+10
-6
@@ -2,27 +2,31 @@ package lmsensors
2
3
var _ Sensor = &IntrusionSensor{}
4
5
-// An IntrusionSensor is a Sensor that detects when the machine's chassis
6
-// has been opened.
5
+// An IntrusionSensor is a Sensor that detects when the machine's chassis has been opened.
6
type IntrusionSensor struct {
7
// The name of the sensor.
8
Name string
9
11
- // Whether or not the machine's chassis has been opened, and the alarm
12
- // has been triggered.
10
+ // A label that describes what the sensor is monitoring. Label may be empty.
11
+ Label string
12
+
13
+ // Whether the machine's chassis has been opened, and the alarm has been triggered.
14
Alarm bool
15
}
16
16
-func (s *IntrusionSensor) name() string { return s.Name }
17
-func (s *IntrusionSensor) setName(name string) { s.Name = name }
17
+func (s *IntrusionSensor) Type() SensorType { return SensorTypeIntrusion }
18
19
func (s *IntrusionSensor) parse(raw map[string]string) error {
20
for k, v := range raw {
21
switch k {
22
case "alarm":
23
s.Alarm = v != "0"
24
+ case "label":
25
+ s.Label = v
26
}
27
}
28
29
return nil
30
}
31
+
32
+func (s *IntrusionSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/lmsensors/powersensor.go
+12
-9
@@ -7,21 +7,21 @@ import (
7
8
var _ Sensor = &PowerSensor{}
9
10
-// A PowerSensor is a Sensor that detects average electrical power consumption
11
-// in watts.
10
+// A PowerSensor is a Sensor that detects average electrical power consumption in watts.
11
type PowerSensor struct {
12
// The name of the sensor.
13
Name string
14
16
- // The average electrical power consumption, in watts, indicated
17
- // by the sensor.
15
+ // A label that describes what the sensor is monitoring. Label may be empty.
16
+ Label string
17
+
18
+ // The average electrical power consumption, in watts, indicated by the sensor.
19
Average float64
20
20
- // The interval of time over which the average electrical power consumption
21
- // is collected.
21
+ // The interval of time over which the average electrical power consumption is collected.
22
AverageInterval time.Duration
23
24
- // Whether or not this sensor has a battery.
24
+ // Whether this sensor has a battery.
25
Battery bool
26
27
// The model number of the sensor.
@@ -34,8 +34,7 @@ type PowerSensor struct {
34
SerialNumber string
35
}
36
37
-func (s *PowerSensor) name() string { return s.Name }
38
-func (s *PowerSensor) setName(name string) { s.Name = name }
37
+func (s *PowerSensor) Type() SensorType { return SensorTypePower }
38
39
func (s *PowerSensor) parse(raw map[string]string) error {
40
for k, v := range raw {
@@ -65,8 +64,12 @@ func (s *PowerSensor) parse(raw map[string]string) error {
64
s.OEMInfo = v
65
case "serial_number":
66
s.SerialNumber = v
67
+ case "label":
68
+ s.Label = v
69
}
70
}
71
72
return nil
73
}
74
+
75
+func (s *PowerSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/lmsensors/scanner.go
+26
-11
@@ -6,6 +6,9 @@ import (
6
"os"
7
"path/filepath"
8
"strings"
9
+ "time"
10
+
11
+ "github.com/netdata/netdata/go/plugins/logger"
12
)
13
14
// A filesystem is an interface to a filesystem, used for testing.
@@ -18,6 +21,8 @@ type filesystem interface {
21
22
// A Scanner scans for Devices, so data can be read from their Sensors.
23
type Scanner struct {
24
+ *logger.Logger
25
+
26
fs filesystem
27
}
28
@@ -29,20 +34,24 @@ func New() *Scanner {
34
}
35
36
// Scan scans for Devices and their Sensors.
32
-func (s *Scanner) Scan() ([]*Device, error) {
33
- paths, err := s.detectDevicePaths()
37
+func (sc *Scanner) Scan() ([]*Device, error) {
38
+ paths, err := sc.detectDevicePaths()
39
if err != nil {
40
return nil, err
41
}
42
43
+ sc.Debugf("sysfs scanner: found %d paths", len(paths))
44
+
45
var devices []*Device
46
47
for _, rootPath := range paths {
48
+ sc.Debugf("sysfs scanner: scanning %s", rootPath)
49
+
50
dev := &Device{}
51
raw := make(map[string]map[string]string)
52
53
// Walk filesystem paths to fetch devices and sensors
45
- err := s.fs.WalkDir(rootPath, func(path string, de fs.DirEntry, err error) error {
54
+ err := sc.fs.WalkDir(rootPath, func(path string, de fs.DirEntry, err error) error {
55
if err != nil {
56
return err
57
}
@@ -60,10 +69,12 @@ func (s *Scanner) Scan() ([]*Device, error) {
69
return nil
70
}
71
63
- s, err := s.fs.ReadFile(path)
72
+ now := time.Now()
73
+ s, err := sc.fs.ReadFile(path)
74
if err != nil {
75
return nil
76
}
77
+ sc.Debugf("sysfs scanner: reading file '%s' took %s", path, time.Since(now))
78
79
if file == "name" {
80
dev.Name = s
@@ -93,6 +104,10 @@ func (s *Scanner) Scan() ([]*Device, error) {
104
return nil, err
105
}
106
107
+ for _, sn := range sensors {
108
+ sc.Debugf("sysfs scanner: found sensor %+v", sn)
109
+ }
110
+
111
dev.Sensors = sensors
112
devices = append(devices, dev)
113
}
@@ -117,11 +132,11 @@ func renameDevices(devices []*Device) {
132
}
133
134
// detectDevicePaths performs a filesystem walk to paths where devices may reside on Linux.
120
-func (s *Scanner) detectDevicePaths() ([]string, error) {
135
+func (sc *Scanner) detectDevicePaths() ([]string, error) {
136
const lookPath = "/sys/class/hwmon"
137
138
var paths []string
124
- err := s.fs.WalkDir(lookPath, func(path string, de os.DirEntry, err error) error {
139
+ err := sc.fs.WalkDir(lookPath, func(path string, de os.DirEntry, err error) error {
140
if err != nil {
141
return err
142
}
@@ -130,7 +145,7 @@ func (s *Scanner) detectDevicePaths() ([]string, error) {
145
return nil
146
}
147
133
- dest, err := s.fs.Readlink(path)
148
+ dest, err := sc.fs.Readlink(path)
149
if err != nil {
150
return err
151
}
@@ -138,7 +153,7 @@ func (s *Scanner) detectDevicePaths() ([]string, error) {
153
dest = filepath.Join(lookPath, filepath.Clean(dest))
154
155
// Symlink destination has a file called name, meaning a sensor exists here and data can be retrieved
141
- fi, err := s.fs.Stat(filepath.Join(dest, "name"))
156
+ fi, err := sc.fs.Stat(filepath.Join(dest, "name"))
157
if err != nil && !os.IsNotExist(err) {
158
return err
159
}
@@ -149,7 +164,7 @@ func (s *Scanner) detectDevicePaths() ([]string, error) {
164
165
// Symlink destination has another symlink called device, which can be read and used to retrieve data
166
device := filepath.Join(dest, "device")
152
- fi, err = s.fs.Stat(device)
167
+ fi, err = sc.fs.Stat(device)
168
if err != nil {
169
if !os.IsNotExist(err) {
170
return err
@@ -161,7 +176,7 @@ func (s *Scanner) detectDevicePaths() ([]string, error) {
176
return nil
177
}
178
164
- device, err = s.fs.Readlink(device)
179
+ device, err = sc.fs.Readlink(device)
180
if err != nil {
181
return err
182
}
@@ -169,7 +184,7 @@ func (s *Scanner) detectDevicePaths() ([]string, error) {
184
dest = filepath.Join(dest, filepath.Clean(device))
185
186
// Symlink destination has a file called name, meaning a sensor exists here and data can be retrieved
172
- if _, err := s.fs.Stat(filepath.Join(dest, "name")); err != nil {
187
+ if _, err := sc.fs.Stat(filepath.Join(dest, "name")); err != nil {
188
if !os.IsNotExist(err) {
189
return err
190
}
src/go/plugin/go.d/modules/sensors/lmsensors/scanner_test.go
+12
-12
@@ -241,7 +241,7 @@ func TestScannerScan(t *testing.T) {
241
Name: "temp1",
242
Label: "Core 0",
243
Input: 40.0,
244
- High: 80.0,
244
+ Maximum: 80.0,
245
Critical: 100.0,
246
CriticalAlarm: false,
247
},
@@ -249,7 +249,7 @@ func TestScannerScan(t *testing.T) {
249
Name: "temp2",
250
Label: "Core 1",
251
Input: 42.0,
252
- High: 80.0,
252
+ Maximum: 80.0,
253
Critical: 100.0,
254
CriticalAlarm: false,
255
},
@@ -404,12 +404,12 @@ func TestScannerScan(t *testing.T) {
404
Alarm: true,
405
},
406
&TemperatureSensor{
407
- Name: "temp1",
408
- Alarm: false,
409
- Beep: true,
410
- Type: TemperatureSensorTypeThermistor,
411
- Input: 43.0,
412
- High: 127.0,
407
+ Name: "temp1",
408
+ Alarm: false,
409
+ Beep: true,
410
+ TempType: TemperatureSensorTypeThermistor,
411
+ Input: 43.0,
412
+ Maximum: 127.0,
413
},
414
},
415
}},
@@ -572,7 +572,7 @@ func TestScannerScan(t *testing.T) {
572
Name: "temp1",
573
Label: "Core 0",
574
Input: 40.0,
575
- High: 80.0,
575
+ Maximum: 80.0,
576
Critical: 100.0,
577
CriticalAlarm: false,
578
},
@@ -580,7 +580,7 @@ func TestScannerScan(t *testing.T) {
580
Name: "temp2",
581
Label: "Core 1",
582
Input: 42.0,
583
- High: 80.0,
583
+ Maximum: 80.0,
584
Critical: 100.0,
585
CriticalAlarm: false,
586
},
@@ -593,7 +593,7 @@ func TestScannerScan(t *testing.T) {
593
Name: "temp1",
594
Label: "Core 0",
595
Input: 38.0,
596
- High: 80.0,
596
+ Maximum: 80.0,
597
Critical: 100.0,
598
CriticalAlarm: false,
599
},
@@ -601,7 +601,7 @@ func TestScannerScan(t *testing.T) {
601
Name: "temp2",
602
Label: "Core 1",
603
Input: 37.0,
604
- High: 80.0,
604
+ Maximum: 80.0,
605
Critical: 100.0,
606
CriticalAlarm: false,
607
},
src/go/plugin/go.d/modules/sensors/lmsensors/sensor.go
+50
-26
@@ -16,53 +16,77 @@ type Device struct {
16
Sensors []Sensor
17
}
18
19
-// A Sensor is a hardware sensor, used to retrieve device temperatures,
20
-// fan speeds, voltages, etc. Use type assertions to check for specific
19
+type SensorType string
20
+
21
+const (
22
+ SensorTypeCurrent SensorType = "current"
23
+ SensorTypeFan SensorType = "fan"
24
+ SensorTypeIntrusion SensorType = "intrusion"
25
+ SensorTypePower SensorType = "power"
26
+ SensorTypeTemperature SensorType = "temperature"
27
+ SensorTypeVoltage SensorType = "voltage"
28
+)
29
+
30
+// A Sensor is a hardware sensor, used to retrieve device temperatures, fan speeds, voltages, etc.
31
+// Use type assertions to check for specific
32
// Sensor types and fetch their data.
33
type Sensor interface {
23
- parse(raw map[string]string) error
24
- name() string
25
- setName(name string)
34
+ Type() SensorType
35
}
36
28
-// parseSensors parses all Sensors from an input raw data slice, produced
29
-// during a filesystem walk.
37
+// parseSensors parses all Sensors from an input raw data slice, produced during a filesystem walk.
38
func parseSensors(raw map[string]map[string]string) ([]Sensor, error) {
39
sensors := make([]Sensor, 0, len(raw))
40
for k, v := range raw {
33
- var s Sensor
41
+ var sn Sensor
42
+ var err error
43
+
44
switch {
45
case strings.HasPrefix(k, "curr"):
36
- s = new(CurrentSensor)
46
+ s := &CurrentSensor{Name: k}
47
+ sn = s
48
+ err = s.parse(v)
49
case strings.HasPrefix(k, "intrusion"):
38
- s = new(IntrusionSensor)
50
+ s := &IntrusionSensor{Name: k}
51
+ sn = s
52
+ err = s.parse(v)
53
case strings.HasPrefix(k, "in"):
40
- s = new(VoltageSensor)
54
+ s := &VoltageSensor{Name: k}
55
+ sn = s
56
+ err = s.parse(v)
57
case strings.HasPrefix(k, "fan"):
42
- s = new(FanSensor)
58
+ s := &FanSensor{Name: k}
59
+ sn = s
60
+ err = s.parse(v)
61
case strings.HasPrefix(k, "power"):
44
- s = new(PowerSensor)
62
+ s := &PowerSensor{Name: k}
63
+ sn = s
64
+ err = s.parse(v)
65
case strings.HasPrefix(k, "temp"):
46
- s = new(TemperatureSensor)
66
+ s := &TemperatureSensor{Name: k}
67
+ sn = s
68
+ err = s.parse(v)
69
default:
70
continue
71
}
50
-
51
- s.setName(k)
52
- if err := s.parse(v); err != nil {
72
+ if err != nil {
73
return nil, err
74
}
75
56
- sensors = append(sensors, s)
76
+ if sn == nil {
77
+ continue
78
+ }
79
+
80
+ sensors = append(sensors, sn)
81
}
82
59
- sort.Sort(byName(sensors))
60
- return sensors, nil
61
-}
83
+ type namer interface{ name() string }
84
63
-// byName implements sort.Interface for []Sensor.
64
-type byName []Sensor
85
+ sort.Slice(sensors, func(i, j int) bool {
86
+ v1, ok1 := sensors[i].(namer)
87
+ v2, ok2 := sensors[j].(namer)
88
+ return ok1 && ok2 && v1.name() < v2.name()
89
+ })
90
66
-func (b byName) Len() int { return len(b) }
67
-func (b byName) Less(i, j int) bool { return b[i].name() < b[j].name() }
68
-func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
91
+ return sensors, nil
92
+}
src/go/plugin/go.d/modules/sensors/lmsensors/temperaturesensor.go
+27
-20
@@ -4,8 +4,7 @@ import (
4
"strconv"
5
)
6
7
-// A TemperatureSensorType is value that indicates the type of a
8
-// TemperatureSensor.
7
+// A TemperatureSensorType is value that indicates the type of TemperatureSensor.
8
type TemperatureSensorType int
9
10
// All possible TemperatureSensorType constants.
@@ -27,42 +26,44 @@ type TemperatureSensor struct {
26
// The name of the sensor.
27
Name string
28
30
- // A label that describes what the sensor is monitoring. Label may be
31
- // empty.
29
+ // A label that describes what the sensor is monitoring. Label may be empty.
30
Label string
31
34
- // Whether or not the sensor has an alarm triggered.
32
+ // Whether the sensor has an alarm triggered.
33
Alarm bool
34
37
- // Whether or not the sensor will sound an audible alarm if an alarm
35
+ // Whether the sensor will sound an audible alarm if an alarm
36
// is triggered.
37
Beep bool
38
41
- // The type of sensor used to report tempearatures.
42
- Type TemperatureSensorType
39
+ // The type of sensor used to report temperatures.
40
+ TempType TemperatureSensorType
41
42
// The input temperature, in degrees Celsius, indicated by the sensor.
43
Input float64
44
47
- // A high threshold temperature, in degrees Celsius, indicated by the
48
- // sensor.
49
- High float64
45
+ // A low threshold temperature, in degrees Celsius, indicated by the sensor.
46
+ Minimum float64
47
51
- // A critical threshold temperature, in degrees Celsius, indicated by the
52
- // sensor.
48
+ // A high threshold temperature, in degrees Celsius, indicated by the sensor.
49
+ Maximum float64
50
+
51
+ // A critical threshold temperature, in degrees Celsius, indicated by the sensor.
52
Critical float64
53
55
- // Whether or not the temperature is past the critical threshold.
54
+ // An emergency threshold temperature, in degrees Celsius, indicated by the sensor.
55
+ Emergency float64
56
+
57
+ // Whether the temperature is past the critical threshold.
58
CriticalAlarm bool
59
}
60
59
-func (s *TemperatureSensor) name() string { return s.Name }
60
-func (s *TemperatureSensor) setName(name string) { s.Name = name }
61
+func (s *TemperatureSensor) Type() SensorType { return SensorTypeTemperature }
62
63
func (s *TemperatureSensor) parse(raw map[string]string) error {
64
for k, v := range raw {
65
switch k {
65
- case "input", "crit", "max":
66
+ case "input", "min", "max", "crit", "emergency":
67
f, err := strconv.ParseFloat(v, 64)
68
if err != nil {
69
return err
@@ -74,10 +75,14 @@ func (s *TemperatureSensor) parse(raw map[string]string) error {
75
switch k {
76
case "input":
77
s.Input = f
78
+ case "min":
79
+ s.Minimum = f
80
+ case "max":
81
+ s.Maximum = f
82
case "crit":
83
s.Critical = f
79
- case "max":
80
- s.High = f
84
+ case "emergency":
85
+ s.Emergency = f
86
}
87
case "alarm":
88
s.Alarm = v != "0"
@@ -89,7 +94,7 @@ func (s *TemperatureSensor) parse(raw map[string]string) error {
94
return err
95
}
96
92
- s.Type = TemperatureSensorType(t)
97
+ s.TempType = TemperatureSensorType(t)
98
case "crit_alarm":
99
s.CriticalAlarm = v != "0"
100
case "label":
@@ -99,3 +104,5 @@ func (s *TemperatureSensor) parse(raw map[string]string) error {
104
105
return nil
106
}
107
+
108
+func (s *TemperatureSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/lmsensors/voltagesensor.go
+12
-7
@@ -11,31 +11,32 @@ type VoltageSensor struct {
11
// The name of the sensor.
12
Name string
13
14
- // A label that describes what the sensor is monitoring. Label may be
15
- // empty.
14
+ // A label that describes what the sensor is monitoring. Label may be empty.
15
Label string
16
18
- // Whether or not the sensor has an alarm triggered.
17
+ // Whether the sensor has an alarm triggered.
18
Alarm bool
19
21
- // Whether or not the sensor will sound an audible alarm when an alarm
20
+ // Whether the sensor will sound an audible alarm when an alarm
21
// is triggered.
22
Beep bool
23
24
// The input voltage indicated by the sensor.
25
Input float64
26
27
+ // The minimum voltage threshold indicated by the sensor.
28
+ Min float64
29
+
30
// The maximum voltage threshold indicated by the sensor.
31
Maximum float64
32
}
33
32
-func (s *VoltageSensor) name() string { return s.Name }
33
-func (s *VoltageSensor) setName(name string) { s.Name = name }
34
+func (s *VoltageSensor) Type() SensorType { return SensorTypeVoltage }
35
36
func (s *VoltageSensor) parse(raw map[string]string) error {
37
for k, v := range raw {
38
switch k {
38
- case "input", "max":
39
+ case "input", "min", "max":
40
f, err := strconv.ParseFloat(v, 64)
41
if err != nil {
42
return err
@@ -47,6 +48,8 @@ func (s *VoltageSensor) parse(raw map[string]string) error {
48
switch k {
49
case "input":
50
s.Input = f
51
+ case "min":
52
+ s.Min = f
53
case "max":
54
s.Maximum = f
55
}
@@ -61,3 +64,5 @@ func (s *VoltageSensor) parse(raw map[string]string) error {
64
65
return nil
66
}
67
+
68
+func (s *VoltageSensor) name() string { return s.Name }
src/go/plugin/go.d/modules/sensors/metadata.yaml
+9
-7
@@ -30,7 +30,7 @@ modules:
30
metrics_description: >
31
This collector gathers real-time system sensor statistics,
32
including temperature, voltage, current, power, fan speed, energy consumption, and humidity,
33
- utilizing the [sensors](https://linux.die.net/man/1/sensors) binary.
33
+ utilizing the [sensors](https://linux.die.net/man/1/sensors) binary or [sysfs](https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface).
34
method_description: ""
35
supported_platforms:
36
include: []
@@ -56,11 +56,7 @@ modules:
56
description: ""
57
setup:
58
prerequisites:
59
- list:
60
- - title: Install lm-sensors
61
- description: |
62
- - Install `lm-sensors` using your distribution's package manager.
63
- - Run `sensors-detect` to detect hardware monitoring chips.
59
+ list: []
60
configuration:
61
file:
62
name: go.d/sensors.conf
@@ -76,7 +72,7 @@ modules:
72
default_value: 10
73
required: false
74
- name: binary_path
79
- description: Path to the `sensors` 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.
75
+ description: Path to the `sensors` binary. If left empty or if the binary is not found, [sysfs](https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface) will be used to collect sensor statistics.
76
default_value: /usr/bin/sensors
77
required: true
78
- name: timeout
@@ -94,6 +90,12 @@ modules:
90
jobs:
91
- name: sensors
92
binary_path: /usr/local/sbin/sensors
93
+ - name: Use sysfs instead of sensors
94
+ description: Set `binary_path` to an empty string to use sysfs.
95
+ config: |
96
+ jobs:
97
+ - name: sensors
98
+ binary_path: ""
99
troubleshooting:
100
problems:
101
list: []
src/go/plugin/go.d/modules/sensors/sensors.go
+14
-11
@@ -8,6 +8,7 @@ import (
8
"time"
9
10
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors/lmsensors"
12
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
13
)
14
@@ -49,13 +50,17 @@ type (
50
51
charts *module.Charts
52
52
- exec sensorsCLI
53
+ exec sensorsBinary
54
+ sc sysfsScanner
55
56
sensors map[string]bool
57
}
56
- sensorsCLI interface {
58
+ sensorsBinary interface {
59
sensorsInfo() ([]byte, error)
60
}
61
+ sysfsScanner interface {
62
+ Scan() ([]*lmsensors.Device, error)
63
+ }
64
)
65
66
func (s *Sensors) Configuration() any {
@@ -63,17 +68,15 @@ func (s *Sensors) Configuration() any {
68
}
69
70
func (s *Sensors) Init() error {
66
- if err := s.validateConfig(); err != nil {
67
- s.Errorf("config validation: %s", err)
68
- return err
71
+ if sb, err := s.initSensorsBinary(); err != nil {
72
+ s.Infof("sensors exec initialization: %v", err)
73
+ } else if sb != nil {
74
+ s.exec = sb
75
}
76
71
- sensorsExec, err := s.initSensorsCliExec()
72
- if err != nil {
73
- s.Errorf("sensors exec initialization: %v", err)
74
- return err
75
- }
76
- s.exec = sensorsExec
77
+ sc := lmsensors.New()
78
+ sc.Logger = s.Logger
79
+ s.sc = sc
80
81
return nil
82
}
src/go/plugin/go.d/modules/sensors/sensors_test.go
+249
-67
@@ -8,6 +8,7 @@ import (
8
"testing"
9
10
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors/lmsensors"
12
13
"github.com/stretchr/testify/assert"
14
"github.com/stretchr/testify/require"
@@ -43,14 +44,14 @@ func TestSensors_Init(t *testing.T) {
44
config Config
45
wantFail bool
46
}{
46
- "fails if 'binary_path' is not set": {
47
- wantFail: true,
47
+ "success if 'binary_path' is not set": {
48
+ wantFail: false,
49
config: Config{
50
BinaryPath: "",
51
},
52
},
52
- "fails if failed to find binary": {
53
- wantFail: true,
53
+ "success if failed to find binary": {
54
+ wantFail: false,
55
config: Config{
56
BinaryPath: "sensors!!!",
57
},
@@ -83,7 +84,7 @@ func TestSensors_Cleanup(t *testing.T) {
84
"after check": {
85
prepare: func() *Sensors {
86
sensors := New()
86
- sensors.exec = prepareMockOkOnlyTemp()
87
+ sensors.exec = prepareMockExecOkOnlyTemp()
88
_ = sensors.Check()
89
return sensors
90
},
@@ -91,7 +92,7 @@ func TestSensors_Cleanup(t *testing.T) {
92
"after collect": {
93
prepare: func() *Sensors {
94
sensors := New()
94
- sensors.exec = prepareMockOkTempInCurrPowerFan()
95
+ sensors.exec = prepareMockExecOkTempInCurrPowerFan()
96
_ = sensors.Collect()
97
return sensors
98
},
@@ -113,28 +114,28 @@ func TestSensors_Charts(t *testing.T) {
114
115
func TestSensors_Check(t *testing.T) {
116
tests := map[string]struct {
116
- prepareMock func() *mockSensorsCLIExec
117
+ prepareMock func() *mockSensorsBinary
118
wantFail bool
119
}{
119
- "only temperature": {
120
+ "exec: only temperature": {
121
wantFail: false,
121
- prepareMock: prepareMockOkOnlyTemp,
122
+ prepareMock: prepareMockExecOkOnlyTemp,
123
},
123
- "temperature and voltage": {
124
+ "exec: temperature and voltage": {
125
wantFail: false,
125
- prepareMock: prepareMockOkTempInCurrPowerFan,
126
+ prepareMock: prepareMockExecOkTempInCurrPowerFan,
127
},
127
- "error on sensors info call": {
128
+ "exec: error on sensors info call": {
129
wantFail: true,
129
- prepareMock: prepareMockErr,
130
+ prepareMock: prepareMockExecErr,
131
},
131
- "empty response": {
132
+ "exec: empty response": {
133
wantFail: true,
133
- prepareMock: prepareMockEmptyResponse,
134
+ prepareMock: prepareMockExecEmptyResponse,
135
},
135
- "unexpected response": {
136
+ "exec: unexpected response": {
137
wantFail: true,
137
- prepareMock: prepareMockUnexpectedResponse,
138
+ prepareMock: prepareMockExecUnexpectedResponse,
139
},
140
}
141
@@ -155,13 +156,14 @@ func TestSensors_Check(t *testing.T) {
156
157
func TestSensors_Collect(t *testing.T) {
158
tests := map[string]struct {
158
- prepareMock func() *mockSensorsCLIExec
159
- wantMetrics map[string]int64
160
- wantCharts int
159
+ prepareExecMock func() *mockSensorsBinary
160
+ prepareSysfsMock func() *mockSysfsScanner
161
+ wantMetrics map[string]int64
162
+ wantCharts int
163
}{
162
- "only temperature": {
163
- prepareMock: prepareMockOkOnlyTemp,
164
- wantCharts: 24,
164
+ "exec: only temperature": {
165
+ prepareExecMock: prepareMockExecOkOnlyTemp,
166
+ wantCharts: 24,
167
wantMetrics: map[string]int64{
168
"sensor_chip_bnxt_en-pci-6200_feature_temp1_subfeature_temp1_input": 80000,
169
"sensor_chip_bnxt_en-pci-6201_feature_temp1_subfeature_temp1_input": 81000,
@@ -189,18 +191,19 @@ func TestSensors_Collect(t *testing.T) {
191
"sensor_chip_nvme-pci-8100_feature_composite_subfeature_temp1_input": 39850,
192
},
193
},
192
- "multiple sensors": {
193
- prepareMock: prepareMockOkTempInCurrPowerFan,
194
- wantCharts: 19,
194
+ "exec: multiple sensors": {
195
+ prepareExecMock: prepareMockExecOkTempInCurrPowerFan,
196
+ wantCharts: 20,
197
wantMetrics: map[string]int64{
198
"sensor_chip_acpitz-acpi-0_feature_temp1_subfeature_temp1_input": 88000,
199
"sensor_chip_amdgpu-pci-0300_feature_edge_subfeature_temp1_input": 53000,
200
"sensor_chip_amdgpu-pci-0300_feature_fan1_subfeature_fan1_input": 0,
201
"sensor_chip_amdgpu-pci-0300_feature_junction_subfeature_temp2_input": 58000,
202
"sensor_chip_amdgpu-pci-0300_feature_mem_subfeature_temp3_input": 57000,
203
+ "sensor_chip_amdgpu-pci-0300_feature_ppt_subfeature_power1_average": 29000,
204
"sensor_chip_amdgpu-pci-0300_feature_vddgfx_subfeature_in0_input": 787,
205
"sensor_chip_amdgpu-pci-6700_feature_edge_subfeature_temp1_input": 60000,
203
- "sensor_chip_amdgpu-pci-6700_feature_ppt_subfeature_power1_input": 8144,
206
+ "sensor_chip_amdgpu-pci-6700_feature_ppt_subfeature_power1_average": 5088,
207
"sensor_chip_amdgpu-pci-6700_feature_vddgfx_subfeature_in0_input": 1335,
208
"sensor_chip_amdgpu-pci-6700_feature_vddnb_subfeature_in1_input": 973,
209
"sensor_chip_asus-isa-0000_feature_cpu_fan_subfeature_fan1_input": 5700000,
@@ -214,25 +217,61 @@ func TestSensors_Collect(t *testing.T) {
217
"sensor_chip_ucsi_source_psy_usbc000:001-isa-0000_feature_in0_subfeature_in0_input": 0,
218
},
219
},
217
- "error on sensors info call": {
218
- prepareMock: prepareMockErr,
219
- wantMetrics: nil,
220
+ "exec: error on sensors info call": {
221
+ prepareExecMock: prepareMockExecErr,
222
+ wantMetrics: nil,
223
},
221
- "empty response": {
222
- prepareMock: prepareMockEmptyResponse,
223
- wantMetrics: nil,
224
+ "exec: empty response": {
225
+ prepareExecMock: prepareMockExecEmptyResponse,
226
+ wantMetrics: nil,
227
},
225
- "unexpected response": {
226
- prepareMock: prepareMockUnexpectedResponse,
227
- wantMetrics: nil,
228
+ "exec: unexpected response": {
229
+ prepareExecMock: prepareMockExecUnexpectedResponse,
230
+ wantMetrics: nil,
231
+ },
232
+
233
+ "sysfs: multiple sensors": {
234
+ prepareSysfsMock: prepareMockSysfsScannerOk,
235
+ wantCharts: 20,
236
+ wantMetrics: map[string]int64{
237
+ "sensor_chip_acpitz-acpi-0_feature_temp1_subfeature_temp1_input": 88000,
238
+ "sensor_chip_amdgpu-pci-0300_feature_edge_subfeature_temp1_input": 53000,
239
+ "sensor_chip_amdgpu-pci-0300_feature_fan1_subfeature_fan1_input": 0,
240
+ "sensor_chip_amdgpu-pci-0300_feature_junction_subfeature_temp2_input": 58000,
241
+ "sensor_chip_amdgpu-pci-0300_feature_mem_subfeature_temp3_input": 57000,
242
+ "sensor_chip_amdgpu-pci-0300_feature_ppt_subfeature_power1_average": 29000,
243
+ "sensor_chip_amdgpu-pci-0300_feature_vddgfx_subfeature_in0_input": 787,
244
+ "sensor_chip_amdgpu-pci-6700_feature_edge_subfeature_temp1_input": 60000,
245
+ "sensor_chip_amdgpu-pci-6700_feature_ppt_subfeature_power1_average": 5088,
246
+ "sensor_chip_amdgpu-pci-6700_feature_vddgfx_subfeature_in0_input": 1335,
247
+ "sensor_chip_amdgpu-pci-6700_feature_vddnb_subfeature_in1_input": 973,
248
+ "sensor_chip_asus-isa-0000_feature_cpu_fan_subfeature_fan1_input": 5700000,
249
+ "sensor_chip_asus-isa-0000_feature_gpu_fan_subfeature_fan2_input": 6600000,
250
+ "sensor_chip_bat0-acpi-0_feature_in0_subfeature_in0_input": 17365,
251
+ "sensor_chip_k10temp-pci-00c3_feature_tctl_subfeature_temp1_input": 90000,
252
+ "sensor_chip_nvme-pci-0600_feature_composite_subfeature_temp1_input": 33850,
253
+ "sensor_chip_nvme-pci-0600_feature_sensor_1_subfeature_temp2_input": 48850,
254
+ "sensor_chip_nvme-pci-0600_feature_sensor_2_subfeature_temp3_input": 33850,
255
+ "sensor_chip_ucsi_source_psy_usbc000:001-isa-0000_feature_curr1_subfeature_curr1_input": 0,
256
+ "sensor_chip_ucsi_source_psy_usbc000:001-isa-0000_feature_in0_subfeature_in0_input": 0,
257
+ },
258
+ },
259
+ "sysfs: error on scan": {
260
+ prepareSysfsMock: prepareMockSysfsScannerErr,
261
+ wantMetrics: nil,
262
},
263
}
264
265
for name, test := range tests {
266
t.Run(name, func(t *testing.T) {
267
sensors := New()
234
- mock := test.prepareMock()
235
- sensors.exec = mock
268
+ if test.prepareExecMock != nil {
269
+ sensors.exec = test.prepareExecMock()
270
+ } else if test.prepareSysfsMock != nil {
271
+ sensors.sc = test.prepareSysfsMock()
272
+ } else {
273
+ t.Fail()
274
+ }
275
276
var mx map[string]int64
277
for i := 0; i < 10; i++ {
@@ -240,48 +279,36 @@ func TestSensors_Collect(t *testing.T) {
279
}
280
281
assert.Equal(t, test.wantMetrics, mx)
282
+
283
assert.Len(t, *sensors.Charts(), test.wantCharts)
244
- testMetricsHasAllChartsDims(t, sensors, mx)
245
- })
246
- }
247
-}
284
249
-func testMetricsHasAllChartsDims(t *testing.T, sensors *Sensors, mx map[string]int64) {
250
- for _, chart := range *sensors.Charts() {
251
- if chart.Obsolete {
252
- continue
253
- }
254
- for _, dim := range chart.Dims {
255
- _, ok := mx[dim.ID]
256
- assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
257
- }
258
- for _, v := range chart.Vars {
259
- _, ok := mx[v.ID]
260
- assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
261
- }
285
+ if len(test.wantMetrics) > 0 {
286
+ module.TestMetricsHasAllChartsDims(t, sensors.Charts(), mx)
287
+ }
288
+ })
289
}
290
}
291
265
-func prepareMockOkOnlyTemp() *mockSensorsCLIExec {
266
- return &mockSensorsCLIExec{
292
+func prepareMockExecOkOnlyTemp() *mockSensorsBinary {
293
+ return &mockSensorsBinary{
294
sensorsInfoData: dataSensorsTemp,
295
}
296
}
297
271
-func prepareMockOkTempInCurrPowerFan() *mockSensorsCLIExec {
272
- return &mockSensorsCLIExec{
298
+func prepareMockExecOkTempInCurrPowerFan() *mockSensorsBinary {
299
+ return &mockSensorsBinary{
300
sensorsInfoData: dataSensorsTempInCurrPowerFan,
301
}
302
}
303
277
-func prepareMockErr() *mockSensorsCLIExec {
278
- return &mockSensorsCLIExec{
304
+func prepareMockExecErr() *mockSensorsBinary {
305
+ return &mockSensorsBinary{
306
errOnSensorsInfo: true,
307
}
308
}
309
283
-func prepareMockUnexpectedResponse() *mockSensorsCLIExec {
284
- return &mockSensorsCLIExec{
310
+func prepareMockExecUnexpectedResponse() *mockSensorsBinary {
311
+ return &mockSensorsBinary{
312
sensorsInfoData: []byte(`
313
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
314
Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
@@ -290,19 +317,174 @@ Fusce et felis pulvinar, posuere sem non, porttitor eros.
317
}
318
}
319
293
-func prepareMockEmptyResponse() *mockSensorsCLIExec {
294
- return &mockSensorsCLIExec{}
320
+func prepareMockExecEmptyResponse() *mockSensorsBinary {
321
+ return &mockSensorsBinary{}
322
}
323
297
-type mockSensorsCLIExec struct {
324
+type mockSensorsBinary struct {
325
errOnSensorsInfo bool
326
sensorsInfoData []byte
327
}
328
302
-func (m *mockSensorsCLIExec) sensorsInfo() ([]byte, error) {
329
+func (m *mockSensorsBinary) sensorsInfo() ([]byte, error) {
330
if m.errOnSensorsInfo {
331
return nil, errors.New("mock.sensorsInfo() error")
332
}
333
334
return m.sensorsInfoData, nil
335
}
336
+
337
+func prepareMockSysfsScannerOk() *mockSysfsScanner {
338
+ return &mockSysfsScanner{
339
+ scanData: []*lmsensors.Device{
340
+ {Name: "asus-isa-0000", Sensors: []lmsensors.Sensor{
341
+ &lmsensors.FanSensor{
342
+ Name: "fan1",
343
+ Label: "cpu_fan",
344
+ Input: 5700,
345
+ },
346
+ &lmsensors.FanSensor{
347
+ Name: "fan2",
348
+ Label: "gpu_fan",
349
+ Input: 6600,
350
+ },
351
+ }},
352
+ {Name: "nvme-pci-0600", Sensors: []lmsensors.Sensor{
353
+ &lmsensors.TemperatureSensor{
354
+ Name: "temp1",
355
+ Label: "Composite",
356
+ Input: 33.85,
357
+ Maximum: 83.85,
358
+ Minimum: -40.15,
359
+ Critical: 87.85,
360
+ Alarm: false,
361
+ },
362
+ &lmsensors.TemperatureSensor{
363
+ Name: "temp2",
364
+ Label: "Sensor 1",
365
+ Input: 48.85,
366
+ Maximum: 65261.85,
367
+ Minimum: -273.15,
368
+ },
369
+ &lmsensors.TemperatureSensor{
370
+ Name: "temp3",
371
+ Label: "Sensor 2",
372
+ Input: 33.85,
373
+ Maximum: 65261.85,
374
+ Minimum: -273.15,
375
+ },
376
+ }},
377
+ {Name: "amdgpu-pci-6700", Sensors: []lmsensors.Sensor{
378
+ &lmsensors.VoltageSensor{
379
+ Name: "in0",
380
+ Label: "vddgfx",
381
+ Input: 1.335,
382
+ },
383
+ &lmsensors.VoltageSensor{
384
+ Name: "in1",
385
+ Label: "vddnb",
386
+ Input: 0.973,
387
+ },
388
+ &lmsensors.TemperatureSensor{
389
+ Name: "temp1",
390
+ Label: "edge",
391
+ Input: 60.000,
392
+ },
393
+ &lmsensors.PowerSensor{
394
+ Name: "power1",
395
+ Label: "PPT",
396
+ Average: 5.088,
397
+ },
398
+ }},
399
+ {Name: "BAT0-acpi-0", Sensors: []lmsensors.Sensor{
400
+ &lmsensors.VoltageSensor{
401
+ Name: "in0",
402
+ Label: "in0",
403
+ Input: 17.365,
404
+ },
405
+ }},
406
+ {Name: "ucsi_source_psy_USBC000:001-isa-0000", Sensors: []lmsensors.Sensor{
407
+ &lmsensors.VoltageSensor{
408
+ Name: "in0",
409
+ Label: "in0",
410
+ Input: 0.000,
411
+ },
412
+ &lmsensors.CurrentSensor{
413
+ Name: "curr1",
414
+ Label: "curr1",
415
+ Input: 0.000,
416
+ },
417
+ }},
418
+ {Name: "k10temp-pci-00c3", Sensors: []lmsensors.Sensor{
419
+ &lmsensors.TemperatureSensor{
420
+ Name: "temp1",
421
+ Label: "Tctl",
422
+ Input: 90,
423
+ },
424
+ }},
425
+ {Name: "amdgpu-pci-0300", Sensors: []lmsensors.Sensor{
426
+ &lmsensors.VoltageSensor{
427
+ Name: "in0",
428
+ Label: "vddgfx",
429
+ Input: 0.787,
430
+ },
431
+ &lmsensors.FanSensor{
432
+ Name: "fan1",
433
+ Label: "fan1",
434
+ Maximum: 4900,
435
+ },
436
+ &lmsensors.TemperatureSensor{
437
+ Name: "temp1",
438
+ Label: "edge",
439
+ Input: 53,
440
+ Critical: 100,
441
+ Emergency: 105,
442
+ },
443
+ &lmsensors.TemperatureSensor{
444
+ Name: "temp2",
445
+ Label: "junction",
446
+ Input: 58,
447
+ Critical: 100,
448
+ Emergency: 105,
449
+ },
450
+ &lmsensors.TemperatureSensor{
451
+ Name: "temp3",
452
+ Label: "mem",
453
+ Input: 57,
454
+ Critical: 106,
455
+ Emergency: 110,
456
+ },
457
+ &lmsensors.PowerSensor{
458
+ Name: "power1",
459
+ Label: "PPT",
460
+ Average: 29,
461
+ },
462
+ }},
463
+ {Name: "acpitz-acpi-0", Sensors: []lmsensors.Sensor{
464
+ &lmsensors.FanSensor{
465
+ Name: "temp1",
466
+ Label: "temp1",
467
+ Input: 88,
468
+ },
469
+ }},
470
+ },
471
+ }
472
+}
473
+
474
+func prepareMockSysfsScannerErr() *mockSysfsScanner {
475
+ return &mockSysfsScanner{
476
+ errOnScan: true,
477
+ }
478
+}
479
+
480
+type mockSysfsScanner struct {
481
+ errOnScan bool
482
+ scanData []*lmsensors.Device
483
+}
484
+
485
+func (m *mockSysfsScanner) Scan() ([]*lmsensors.Device, error) {
486
+ if m.errOnScan {
487
+ return nil, errors.New("mock.scan() error")
488
+ }
489
+ return m.scanData, nil
490
+}