@cryptotaxi247 / netdata-1 / commits / c3261e3b6

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 [![Build Status](https://travis-ci.org/mdlayher/lmsensors.svg?branch=master)](https://travis-ci.org/mdlayher/lmsensors) [![GoDoc](http://godoc.org/github.com/mdlayher/lmsensors?status.svg)](http://godoc.org/github.com/mdlayher/lmsensors) [![Report Card](https://goreportcard.com/badge/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 +}