go.d add sensors (#17466)
Ilya Mashchenko committed
Apr 22, 2024 at 11:32 UTC
b7c4d442c5e0f61ba5eacd042d921955b3e763b3
17 files changed
+1211
-2
src/go/collectors/go.d.plugin/README.md
+2
@@ -76,6 +76,7 @@ see the appropriate collector readme.
76
| [fluentd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fluentd) | Fluentd |
77
| [freeradius](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/freeradius) | FreeRADIUS |
78
| [haproxy](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/haproxy) | HAProxy |
79
+| [hddtemp](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hddtemp) | Disks temperature |
80
| [hdfs](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hdfs) | HDFS |
81
| [httpcheck](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/httpcheck) | Any HTTP Endpoint |
82
| [intelgpu](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/intelgpu) | Intel integrated GPU |
@@ -113,6 +114,7 @@ see the appropriate collector readme.
114
| [rabbitmq](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/rabbitmq) | RabbitMQ |
115
| [redis](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/redis) | Redis |
116
| [scaleio](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/scaleio) | Dell EMC ScaleIO |
117
+| [sensors](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules) | Hardware Sensors |
118
| [SNMP](https://github.com/netdata/netdata/blob/master/src/go/collectors/go.d.plugin/modules/snmp) | SNMP |
119
| [squidlog](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/squidlog) | Squid |
120
| [storcli](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/storcli) | Broadcom Hardware RAID |
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -76,6 +76,7 @@ modules:
76
# rabbitmq: yes
77
# redis: yes
78
# scaleio: yes
79
+# sensors: yes
80
# snmp: yes
81
# squidlog: yes
82
# storcli: yes
src/go/collectors/go.d.plugin/config/go.d/sensors.conf
new
+6
@@ -0,0 +1,6 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/sensors#readme
3
+
4
+jobs:
5
+ - name: sensors
6
+ binary_path: /usr/bin/sensors
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -68,6 +68,7 @@ import (
68
_ "github.com/netdata/netdata/go/go.d.plugin/modules/rabbitmq"
69
_ "github.com/netdata/netdata/go/go.d.plugin/modules/redis"
70
_ "github.com/netdata/netdata/go/go.d.plugin/modules/scaleio"
71
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/sensors"
72
_ "github.com/netdata/netdata/go/go.d.plugin/modules/snmp"
73
_ "github.com/netdata/netdata/go/go.d.plugin/modules/squidlog"
74
_ "github.com/netdata/netdata/go/go.d.plugin/modules/storcli"
src/go/collectors/go.d.plugin/modules/sensors/charts.go
new
+159
@@ -0,0 +1,159 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
10
+)
11
+
12
+const (
13
+ prioSensorTemperature = module.Priority + iota
14
+ prioSensorVoltage
15
+ prioSensorCurrent
16
+ prioSensorPower
17
+ prioSensorFan
18
+ prioSensorEnergy
19
+ prioSensorHumidity
20
+)
21
+
22
+var sensorTemperatureChartTmpl = module.Chart{
23
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_temperature",
24
+ Title: "Sensor temperature",
25
+ Units: "Celsius",
26
+ Fam: "temperature",
27
+ Ctx: "sensors.sensor_temperature",
28
+ Type: module.Line,
29
+ Priority: prioSensorTemperature,
30
+ Dims: module.Dims{
31
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "temperature", Div: precision},
32
+ },
33
+}
34
+
35
+var sensorVoltageChartTmpl = module.Chart{
36
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_voltage",
37
+ Title: "Sensor voltage",
38
+ Units: "Volts",
39
+ Fam: "voltage",
40
+ Ctx: "sensors.sensor_voltage",
41
+ Type: module.Line,
42
+ Priority: prioSensorVoltage,
43
+ Dims: module.Dims{
44
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "voltage", Div: precision},
45
+ },
46
+}
47
+
48
+var sensorCurrentChartTmpl = module.Chart{
49
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_current",
50
+ Title: "Sensor current",
51
+ Units: "Amperes",
52
+ Fam: "current",
53
+ Ctx: "sensors.sensor_current",
54
+ Type: module.Line,
55
+ Priority: prioSensorCurrent,
56
+ Dims: module.Dims{
57
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "current", Div: precision},
58
+ },
59
+}
60
+
61
+var sensorPowerChartTmpl = module.Chart{
62
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_power",
63
+ Title: "Sensor power",
64
+ Units: "Watts",
65
+ Fam: "power",
66
+ Ctx: "sensors.sensor_power",
67
+ Type: module.Line,
68
+ Priority: prioSensorPower,
69
+ Dims: module.Dims{
70
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "power", Div: precision},
71
+ },
72
+}
73
+
74
+var sensorFanChartTmpl = module.Chart{
75
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_fan",
76
+ Title: "Sensor fan speed",
77
+ Units: "RPM",
78
+ Fam: "fan",
79
+ Ctx: "sensors.sensor_fan_speed",
80
+ Type: module.Line,
81
+ Priority: prioSensorFan,
82
+ Dims: module.Dims{
83
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "fan", Div: precision},
84
+ },
85
+}
86
+
87
+var sensorEnergyChartTmpl = module.Chart{
88
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_energy",
89
+ Title: "Sensor energy",
90
+ Units: "Joules",
91
+ Fam: "energy",
92
+ Ctx: "sensors.sensor_energy",
93
+ Type: module.Line,
94
+ Priority: prioSensorEnergy,
95
+ Dims: module.Dims{
96
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "energy", Div: precision},
97
+ },
98
+}
99
+
100
+var sensorHumidityChartTmpl = module.Chart{
101
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_humidity",
102
+ Title: "Sensor humidity",
103
+ Units: "percent",
104
+ Fam: "humidity",
105
+ Ctx: "sensors.sensor_humidity",
106
+ Type: module.Area,
107
+ Priority: prioSensorHumidity,
108
+ Dims: module.Dims{
109
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "humidity", Div: precision},
110
+ },
111
+}
112
+
113
+func (s *Sensors) addSensorChart(sn sensorStats) {
114
+ var chart *module.Chart
115
+
116
+ switch sensorType(sn) {
117
+ case sensorTypeTemp:
118
+ chart = sensorTemperatureChartTmpl.Copy()
119
+ case sensorTypeVoltage:
120
+ chart = sensorVoltageChartTmpl.Copy()
121
+ case sensorTypePower:
122
+ chart = sensorPowerChartTmpl.Copy()
123
+ case sensorTypeHumidity:
124
+ chart = sensorHumidityChartTmpl.Copy()
125
+ case sensorTypeFan:
126
+ chart = sensorFanChartTmpl.Copy()
127
+ case sensorTypeCurrent:
128
+ chart = sensorCurrentChartTmpl.Copy()
129
+ case sensorTypeEnergy:
130
+ chart = sensorEnergyChartTmpl.Copy()
131
+ default:
132
+ return
133
+ }
134
+
135
+ chip, feat, subfeat := snakeCase(sn.chip), snakeCase(sn.feature), snakeCase(sn.subfeature)
136
+
137
+ chart.ID = fmt.Sprintf(chart.ID, chip, feat, subfeat)
138
+ chart.Labels = []module.Label{
139
+ {Key: "chip", Value: sn.chip},
140
+ {Key: "feature", Value: sn.feature},
141
+ }
142
+ for _, dim := range chart.Dims {
143
+ dim.ID = fmt.Sprintf(dim.ID, chip, feat, subfeat)
144
+ }
145
+
146
+ if err := s.Charts().Add(chart); err != nil {
147
+ s.Warning(err)
148
+ }
149
+}
150
+
151
+func (s *Sensors) removeSensorChart(px string) {
152
+ for _, chart := range *s.Charts() {
153
+ if strings.HasPrefix(chart.ID, px) {
154
+ chart.MarkRemove()
155
+ chart.MarkNotCreated()
156
+ return
157
+ }
158
+ }
159
+}
src/go/collectors/go.d.plugin/modules/sensors/collect.go
new
+179
@@ -0,0 +1,179 @@
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
+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
+
35
+const precision = 1000
36
+
37
+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)
91
+ }
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
179
+}
src/go/collectors/go.d.plugin/modules/sensors/config_schema.json
new
+47
@@ -0,0 +1,47 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Sensors collector configuration",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 10
13
+ },
14
+ "binary_path": {
15
+ "title": "Binary path",
16
+ "description": "Path to the `sensors` binary.",
17
+ "type": "string",
18
+ "default": "/usr/bin/sensors"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for executing the binary, specified in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 2
26
+ }
27
+ },
28
+ "required": [
29
+ "binary_path"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "binary_path": {
41
+ "ui:help": "If an absolute path is provided, the collector will use it directly; otherwise, it will search for the binary in directories specified in the PATH environment variable."
42
+ },
43
+ "timeout": {
44
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
45
+ }
46
+ }
47
+}
src/go/collectors/go.d.plugin/modules/sensors/exec.go
new
+41
@@ -0,0 +1,41 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "context"
7
+ "fmt"
8
+ "os/exec"
9
+ "time"
10
+
11
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
12
+)
13
+
14
+func newSensorsCliExec(binPath string, timeout time.Duration) *sensorsCliExec {
15
+ return &sensorsCliExec{
16
+ binPath: binPath,
17
+ timeout: timeout,
18
+ }
19
+}
20
+
21
+type sensorsCliExec struct {
22
+ *logger.Logger
23
+
24
+ binPath string
25
+ timeout time.Duration
26
+}
27
+
28
+func (e *sensorsCliExec) sensorsInfo() ([]byte, error) {
29
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
30
+ defer cancel()
31
+
32
+ cmd := exec.CommandContext(ctx, e.binPath, "-A", "-u")
33
+ e.Debugf("executing '%s'", cmd)
34
+
35
+ bs, err := cmd.Output()
36
+ if err != nil {
37
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
38
+ }
39
+
40
+ return bs, nil
41
+}
src/go/collectors/go.d.plugin/modules/sensors/init.go
new
+38
@@ -0,0 +1,38 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+)
11
+
12
+func (s *Sensors) validateConfig() error {
13
+ if s.BinaryPath == "" {
14
+ return errors.New("no sensors binary path specified")
15
+ }
16
+ return nil
17
+}
18
+
19
+func (s *Sensors) initSensorsCliExec() (sensorsCLI, error) {
20
+ binPath := s.BinaryPath
21
+
22
+ if !strings.HasPrefix(binPath, "/") {
23
+ path, err := exec.LookPath(binPath)
24
+ if err != nil {
25
+ return nil, err
26
+ }
27
+ binPath = path
28
+ }
29
+
30
+ if _, err := os.Stat(binPath); err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ sensorsExec := newSensorsCliExec(binPath, s.Timeout.Duration())
35
+ sensorsExec.Logger = s.Logger
36
+
37
+ return sensorsExec, nil
38
+}
src/go/collectors/go.d.plugin/modules/sensors/metadata.yaml
new
+157
@@ -0,0 +1,157 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-sensors
5
+ plugin_name: go.d.plugin
6
+ module_name: sensors
7
+ monitored_instance:
8
+ name: Linux Sensors (lm-sensors)
9
+ link: https://hwmon.wiki.kernel.org/lm_sensors
10
+ icon_filename: "microchip.svg"
11
+ categories:
12
+ - data-collection.hardware-devices-and-sensors
13
+ keywords:
14
+ - sensors
15
+ - temperature
16
+ - voltage
17
+ - current
18
+ - power
19
+ - fan
20
+ - energy
21
+ - humidity
22
+ related_resources:
23
+ integrations:
24
+ list: []
25
+ info_provided_to_referring_integrations:
26
+ description: ""
27
+ most_popular: false
28
+ overview:
29
+ data_collection:
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.
34
+ method_description: ""
35
+ supported_platforms:
36
+ include: []
37
+ exclude: []
38
+ multi_instance: false
39
+ additional_permissions:
40
+ description: ""
41
+ default_behavior:
42
+ auto_detection:
43
+ description: |
44
+ The following type of sensors are auto-detected:
45
+
46
+ - temperature
47
+ - fan
48
+ - voltage
49
+ - current
50
+ - power
51
+ - energy
52
+ - humidity
53
+ limits:
54
+ description: ""
55
+ performance_impact:
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.
64
+ configuration:
65
+ file:
66
+ name: go.d/sensors.conf
67
+ options:
68
+ description: |
69
+ The following options can be defined globally: update_every.
70
+ folding:
71
+ title: Config options
72
+ enabled: true
73
+ list:
74
+ - name: update_every
75
+ description: Data collection frequency.
76
+ default_value: 10
77
+ required: false
78
+ - 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.
80
+ default_value: /usr/bin/sensors
81
+ required: true
82
+ - name: timeout
83
+ description: Timeout for executing the binary, specified in seconds.
84
+ default_value: 2
85
+ required: false
86
+ examples:
87
+ folding:
88
+ title: Config
89
+ enabled: true
90
+ list:
91
+ - name: Custom binary path
92
+ description: The executable is not in the directories specified in the PATH environment variable.
93
+ config: |
94
+ jobs:
95
+ - name: sensors
96
+ binary_path: /usr/local/sbin/sensors
97
+ troubleshooting:
98
+ problems:
99
+ list: []
100
+ alerts: []
101
+ metrics:
102
+ folding:
103
+ title: Metrics
104
+ enabled: false
105
+ description: ""
106
+ availability: []
107
+ scopes:
108
+ - name: sensor
109
+ description: These metrics refer to the sensor.
110
+ labels:
111
+ - name: chip
112
+ description: The hardware component responsible for the sensor monitoring.
113
+ - name: feature
114
+ description: The specific sensor or monitoring point provided by the chip.
115
+ metrics:
116
+ - name: sensors.sensor_temperature
117
+ description: Sensor temperature
118
+ unit: Celsius
119
+ chart_type: line
120
+ dimensions:
121
+ - name: temperature
122
+ - name: sensors.sensor_voltage
123
+ description: Sensor voltage
124
+ unit: Volts
125
+ chart_type: line
126
+ dimensions:
127
+ - name: voltage
128
+ - name: sensors.sensor_current
129
+ description: Sensor current
130
+ unit: Amperes
131
+ chart_type: line
132
+ dimensions:
133
+ - name: current
134
+ - name: sensors.sensor_power
135
+ description: Sensor power
136
+ unit: Watts
137
+ chart_type: line
138
+ dimensions:
139
+ - name: power
140
+ - name: sensors.sensor_fan_speed
141
+ description: Sensor fan speed
142
+ unit: RPM
143
+ chart_type: line
144
+ dimensions:
145
+ - name: fan
146
+ - name: sensors.sensor_energy
147
+ description: Sensor energy
148
+ unit: Joules
149
+ chart_type: line
150
+ dimensions:
151
+ - name: energy
152
+ - name: sensors.sensor_humidity
153
+ description: Sensor humidity
154
+ unit: percent
155
+ chart_type: area
156
+ dimensions:
157
+ - name: humidity
src/go/collectors/go.d.plugin/modules/sensors/sensors.go
new
+111
@@ -0,0 +1,111 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("sensors", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ })
25
+}
26
+
27
+func New() *Sensors {
28
+ return &Sensors{
29
+ Config: Config{
30
+ BinaryPath: "/usr/bin/sensors",
31
+ Timeout: web.Duration(time.Second * 2),
32
+ },
33
+ charts: &module.Charts{},
34
+ sensors: make(map[string]bool),
35
+ }
36
+}
37
+
38
+type Config struct {
39
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
40
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
41
+ BinaryPath string `yaml:"binary_path" json:"binary_path"`
42
+}
43
+
44
+type (
45
+ Sensors struct {
46
+ module.Base
47
+ Config `yaml:",inline" json:""`
48
+
49
+ charts *module.Charts
50
+
51
+ exec sensorsCLI
52
+
53
+ sensors map[string]bool
54
+ }
55
+ sensorsCLI interface {
56
+ sensorsInfo() ([]byte, error)
57
+ }
58
+)
59
+
60
+func (s *Sensors) Configuration() any {
61
+ return s.Config
62
+}
63
+
64
+func (s *Sensors) Init() error {
65
+ if err := s.validateConfig(); err != nil {
66
+ s.Errorf("config validation: %s", err)
67
+ return err
68
+ }
69
+
70
+ sensorsExec, err := s.initSensorsCliExec()
71
+ if err != nil {
72
+ s.Errorf("sensors exec initialization: %v", err)
73
+ return err
74
+ }
75
+ s.exec = sensorsExec
76
+
77
+ return nil
78
+}
79
+
80
+func (s *Sensors) Check() error {
81
+ mx, err := s.collect()
82
+ if err != nil {
83
+ s.Error(err)
84
+ return err
85
+ }
86
+
87
+ if len(mx) == 0 {
88
+ return errors.New("no metrics collected")
89
+ }
90
+
91
+ return nil
92
+}
93
+
94
+func (s *Sensors) Charts() *module.Charts {
95
+ return s.charts
96
+}
97
+
98
+func (s *Sensors) Collect() map[string]int64 {
99
+ mx, err := s.collect()
100
+ if err != nil {
101
+ s.Error(err)
102
+ }
103
+
104
+ if len(mx) == 0 {
105
+ return nil
106
+ }
107
+
108
+ return mx
109
+}
110
+
111
+func (s *Sensors) Cleanup() {}
src/go/collectors/go.d.plugin/modules/sensors/sensors_test.go
new
+308
@@ -0,0 +1,308 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package sensors
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "testing"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+
12
+ "github.com/stretchr/testify/assert"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+var (
17
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19
+
20
+ dataSensorsTemp, _ = os.ReadFile("testdata/sensors-temp.txt")
21
+ dataSensorsTempInCurrPowerFan, _ = os.ReadFile("testdata/sensors-temp-in-curr-power-fan.txt")
22
+)
23
+
24
+func Test_testDataIsValid(t *testing.T) {
25
+ for name, data := range map[string][]byte{
26
+ "dataConfigJSON": dataConfigJSON,
27
+ "dataConfigYAML": dataConfigYAML,
28
+
29
+ "dataSensorsTemp": dataSensorsTemp,
30
+ "dataSensorsTempInCurrPowerFan": dataSensorsTempInCurrPowerFan,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+
34
+ }
35
+}
36
+
37
+func TestSensors_Configuration(t *testing.T) {
38
+ module.TestConfigurationSerialize(t, &Sensors{}, dataConfigJSON, dataConfigYAML)
39
+}
40
+
41
+func TestSensors_Init(t *testing.T) {
42
+ tests := map[string]struct {
43
+ config Config
44
+ wantFail bool
45
+ }{
46
+ "fails if 'binary_path' is not set": {
47
+ wantFail: true,
48
+ config: Config{
49
+ BinaryPath: "",
50
+ },
51
+ },
52
+ "fails if failed to find binary": {
53
+ wantFail: true,
54
+ config: Config{
55
+ BinaryPath: "sensors!!!",
56
+ },
57
+ },
58
+ }
59
+
60
+ for name, test := range tests {
61
+ t.Run(name, func(t *testing.T) {
62
+ sensors := New()
63
+ sensors.Config = test.config
64
+
65
+ if test.wantFail {
66
+ assert.Error(t, sensors.Init())
67
+ } else {
68
+ assert.NoError(t, sensors.Init())
69
+ }
70
+ })
71
+ }
72
+}
73
+
74
+func TestSensors_Cleanup(t *testing.T) {
75
+ tests := map[string]struct {
76
+ prepare func() *Sensors
77
+ }{
78
+ "not initialized exec": {
79
+ prepare: func() *Sensors {
80
+ return New()
81
+ },
82
+ },
83
+ "after check": {
84
+ prepare: func() *Sensors {
85
+ sensors := New()
86
+ sensors.exec = prepareMockOkOnlyTemp()
87
+ _ = sensors.Check()
88
+ return sensors
89
+ },
90
+ },
91
+ "after collect": {
92
+ prepare: func() *Sensors {
93
+ sensors := New()
94
+ sensors.exec = prepareMockOkTempInCurrPowerFan()
95
+ _ = sensors.Collect()
96
+ return sensors
97
+ },
98
+ },
99
+ }
100
+
101
+ for name, test := range tests {
102
+ t.Run(name, func(t *testing.T) {
103
+ sensors := test.prepare()
104
+
105
+ assert.NotPanics(t, sensors.Cleanup)
106
+ })
107
+ }
108
+}
109
+
110
+func TestSensors_Charts(t *testing.T) {
111
+ assert.NotNil(t, New().Charts())
112
+}
113
+
114
+func TestSensors_Check(t *testing.T) {
115
+ tests := map[string]struct {
116
+ prepareMock func() *mockSensorsCLIExec
117
+ wantFail bool
118
+ }{
119
+ "only temperature": {
120
+ wantFail: false,
121
+ prepareMock: prepareMockOkOnlyTemp,
122
+ },
123
+ "temperature and voltage": {
124
+ wantFail: false,
125
+ prepareMock: prepareMockOkTempInCurrPowerFan,
126
+ },
127
+ "error on sensors info call": {
128
+ wantFail: true,
129
+ prepareMock: prepareMockErr,
130
+ },
131
+ "empty response": {
132
+ wantFail: true,
133
+ prepareMock: prepareMockEmptyResponse,
134
+ },
135
+ "unexpected response": {
136
+ wantFail: true,
137
+ prepareMock: prepareMockUnexpectedResponse,
138
+ },
139
+ }
140
+
141
+ for name, test := range tests {
142
+ t.Run(name, func(t *testing.T) {
143
+ sensors := New()
144
+ mock := test.prepareMock()
145
+ sensors.exec = mock
146
+
147
+ if test.wantFail {
148
+ assert.Error(t, sensors.Check())
149
+ } else {
150
+ assert.NoError(t, sensors.Check())
151
+ }
152
+ })
153
+ }
154
+}
155
+
156
+func TestSensors_Collect(t *testing.T) {
157
+ tests := map[string]struct {
158
+ prepareMock func() *mockSensorsCLIExec
159
+ wantMetrics map[string]int64
160
+ wantCharts int
161
+ }{
162
+ "only temperature": {
163
+ prepareMock: prepareMockOkOnlyTemp,
164
+ wantCharts: 24,
165
+ wantMetrics: map[string]int64{
166
+ "sensor_chip_bnxt_en-pci-6200_feature_temp1_subfeature_temp1_input": 80000,
167
+ "sensor_chip_bnxt_en-pci-6201_feature_temp1_subfeature_temp1_input": 81000,
168
+ "sensor_chip_k10temp-pci-00c3_feature_tccd1_subfeature_temp3_input": 58250,
169
+ "sensor_chip_k10temp-pci-00c3_feature_tccd2_subfeature_temp4_input": 60250,
170
+ "sensor_chip_k10temp-pci-00c3_feature_tccd3_subfeature_temp5_input": 57000,
171
+ "sensor_chip_k10temp-pci-00c3_feature_tccd4_subfeature_temp6_input": 57250,
172
+ "sensor_chip_k10temp-pci-00c3_feature_tccd5_subfeature_temp7_input": 57750,
173
+ "sensor_chip_k10temp-pci-00c3_feature_tccd6_subfeature_temp8_input": 59500,
174
+ "sensor_chip_k10temp-pci-00c3_feature_tccd7_subfeature_temp9_input": 58500,
175
+ "sensor_chip_k10temp-pci-00c3_feature_tccd8_subfeature_temp10_input": 61250,
176
+ "sensor_chip_k10temp-pci-00c3_feature_tctl_subfeature_temp1_input": 62000,
177
+ "sensor_chip_k10temp-pci-00cb_feature_tccd1_subfeature_temp3_input": 54000,
178
+ "sensor_chip_k10temp-pci-00cb_feature_tccd2_subfeature_temp4_input": 55500,
179
+ "sensor_chip_k10temp-pci-00cb_feature_tccd3_subfeature_temp5_input": 56000,
180
+ "sensor_chip_k10temp-pci-00cb_feature_tccd4_subfeature_temp6_input": 52750,
181
+ "sensor_chip_k10temp-pci-00cb_feature_tccd5_subfeature_temp7_input": 53500,
182
+ "sensor_chip_k10temp-pci-00cb_feature_tccd6_subfeature_temp8_input": 55250,
183
+ "sensor_chip_k10temp-pci-00cb_feature_tccd7_subfeature_temp9_input": 53000,
184
+ "sensor_chip_k10temp-pci-00cb_feature_tccd8_subfeature_temp10_input": 53750,
185
+ "sensor_chip_k10temp-pci-00cb_feature_tctl_subfeature_temp1_input": 57500,
186
+ "sensor_chip_nouveau-pci-4100_feature_temp1_subfeature_temp1_input": 51000,
187
+ "sensor_chip_nvme-pci-0100_feature_composite_subfeature_temp1_input": 39850,
188
+ "sensor_chip_nvme-pci-6100_feature_composite_subfeature_temp1_input": 48850,
189
+ "sensor_chip_nvme-pci-8100_feature_composite_subfeature_temp1_input": 39850,
190
+ },
191
+ },
192
+ "multiple sensors": {
193
+ prepareMock: prepareMockOkTempInCurrPowerFan,
194
+ wantCharts: 19,
195
+ wantMetrics: map[string]int64{
196
+ "sensor_chip_acpitz-acpi-0_feature_temp1_subfeature_temp1_input": 88000,
197
+ "sensor_chip_amdgpu-pci-0300_feature_edge_subfeature_temp1_input": 53000,
198
+ "sensor_chip_amdgpu-pci-0300_feature_fan1_subfeature_fan1_input": 0,
199
+ "sensor_chip_amdgpu-pci-0300_feature_junction_subfeature_temp2_input": 58000,
200
+ "sensor_chip_amdgpu-pci-0300_feature_mem_subfeature_temp3_input": 57000,
201
+ "sensor_chip_amdgpu-pci-0300_feature_vddgfx_subfeature_in0_input": 787,
202
+ "sensor_chip_amdgpu-pci-6700_feature_edge_subfeature_temp1_input": 60000,
203
+ "sensor_chip_amdgpu-pci-6700_feature_ppt_subfeature_power1_input": 8144,
204
+ "sensor_chip_amdgpu-pci-6700_feature_vddgfx_subfeature_in0_input": 1335,
205
+ "sensor_chip_amdgpu-pci-6700_feature_vddnb_subfeature_in1_input": 973,
206
+ "sensor_chip_asus-isa-0000_feature_cpu_fan_subfeature_fan1_input": 5700000,
207
+ "sensor_chip_asus-isa-0000_feature_gpu_fan_subfeature_fan2_input": 6600000,
208
+ "sensor_chip_bat0-acpi-0_feature_in0_subfeature_in0_input": 17365,
209
+ "sensor_chip_k10temp-pci-00c3_feature_tctl_subfeature_temp1_input": 90000,
210
+ "sensor_chip_nvme-pci-0600_feature_composite_subfeature_temp1_input": 33850,
211
+ "sensor_chip_nvme-pci-0600_feature_sensor_1_subfeature_temp2_input": 48850,
212
+ "sensor_chip_nvme-pci-0600_feature_sensor_2_subfeature_temp3_input": 33850,
213
+ "sensor_chip_ucsi_source_psy_usbc000:001-isa-0000_feature_curr1_subfeature_curr1_input": 0,
214
+ "sensor_chip_ucsi_source_psy_usbc000:001-isa-0000_feature_in0_subfeature_in0_input": 0,
215
+ },
216
+ },
217
+ "error on sensors info call": {
218
+ prepareMock: prepareMockErr,
219
+ wantMetrics: nil,
220
+ },
221
+ "empty response": {
222
+ prepareMock: prepareMockEmptyResponse,
223
+ wantMetrics: nil,
224
+ },
225
+ "unexpected response": {
226
+ prepareMock: prepareMockUnexpectedResponse,
227
+ wantMetrics: nil,
228
+ },
229
+ }
230
+
231
+ for name, test := range tests {
232
+ t.Run(name, func(t *testing.T) {
233
+ sensors := New()
234
+ mock := test.prepareMock()
235
+ sensors.exec = mock
236
+
237
+ var mx map[string]int64
238
+ for i := 0; i < 10; i++ {
239
+ mx = sensors.Collect()
240
+ }
241
+
242
+ assert.Equal(t, test.wantMetrics, mx)
243
+ assert.Len(t, *sensors.Charts(), test.wantCharts)
244
+ testMetricsHasAllChartsDims(t, sensors, mx)
245
+ })
246
+ }
247
+}
248
+
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
+ }
262
+ }
263
+}
264
+
265
+func prepareMockOkOnlyTemp() *mockSensorsCLIExec {
266
+ return &mockSensorsCLIExec{
267
+ sensorsInfoData: dataSensorsTemp,
268
+ }
269
+}
270
+
271
+func prepareMockOkTempInCurrPowerFan() *mockSensorsCLIExec {
272
+ return &mockSensorsCLIExec{
273
+ sensorsInfoData: dataSensorsTempInCurrPowerFan,
274
+ }
275
+}
276
+
277
+func prepareMockErr() *mockSensorsCLIExec {
278
+ return &mockSensorsCLIExec{
279
+ errOnSensorsInfo: true,
280
+ }
281
+}
282
+
283
+func prepareMockUnexpectedResponse() *mockSensorsCLIExec {
284
+ return &mockSensorsCLIExec{
285
+ sensorsInfoData: []byte(`
286
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
287
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
288
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
289
+`),
290
+ }
291
+}
292
+
293
+func prepareMockEmptyResponse() *mockSensorsCLIExec {
294
+ return &mockSensorsCLIExec{}
295
+}
296
+
297
+type mockSensorsCLIExec struct {
298
+ errOnSensorsInfo bool
299
+ sensorsInfoData []byte
300
+}
301
+
302
+func (m *mockSensorsCLIExec) sensorsInfo() ([]byte, error) {
303
+ if m.errOnSensorsInfo {
304
+ return nil, errors.New("mock.sensorsInfo() error")
305
+ }
306
+
307
+ return m.sensorsInfoData, nil
308
+}
src/go/collectors/go.d.plugin/modules/sensors/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123,
4
+ "binary_path": "ok"
5
+}
src/go/collectors/go.d.plugin/modules/sensors/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+timeout: 123.123
3
+binary_path: "ok"
src/go/collectors/go.d.plugin/modules/sensors/testdata/sensors-temp-in-curr-power-fan.txt
new
+72
@@ -0,0 +1,72 @@
1
+asus-isa-0000
2
+cpu_fan:
3
+ fan1_input: 5700.000
4
+gpu_fan:
5
+ fan2_input: 6600.000
6
+nvme-pci-0600
7
+Composite:
8
+ temp1_input: 33.850
9
+ temp1_max: 83.850
10
+ temp1_min: -40.150
11
+ temp1_crit: 87.850
12
+ temp1_alarm: 0.000
13
+Sensor 1:
14
+ temp2_input: 48.850
15
+ temp2_max: 65261.850
16
+ temp2_min: -273.150
17
+Sensor 2:
18
+ temp3_input: 33.850
19
+ temp3_max: 65261.850
20
+ temp3_min: -273.150
21
+amdgpu-pci-6700
22
+vddgfx:
23
+ in0_input: 1.335
24
+vddnb:
25
+ in1_input: 0.973
26
+edge:
27
+ temp1_input: 60.000
28
+PPT:
29
+ power1_average: 5.088
30
+ power1_input: 8.144
31
+BAT0-acpi-0
32
+in0:
33
+ in0_input: 17.365
34
+ucsi_source_psy_USBC000:001-isa-0000
35
+in0:
36
+ in0_input: 0.000
37
+ in0_min: 0.000
38
+ in0_max: 0.000
39
+curr1:
40
+ curr1_input: 0.000
41
+ curr1_max: 0.000
42
+k10temp-pci-00c3
43
+Tctl:
44
+ temp1_input: 90.000
45
+amdgpu-pci-0300
46
+vddgfx:
47
+ in0_input: 0.787
48
+fan1:
49
+ fan1_input: 0.000
50
+ fan1_min: 0.000
51
+ fan1_max: 4900.000
52
+edge:
53
+ temp1_input: 53.000
54
+ temp1_crit: 100.000
55
+ temp1_crit_hyst: -273.150
56
+ temp1_emergency: 105.000
57
+junction:
58
+ temp2_input: 58.000
59
+ temp2_crit: 100.000
60
+ temp2_crit_hyst: -273.150
61
+ temp2_emergency: 105.000
62
+mem:
63
+ temp3_input: 57.000
64
+ temp3_crit: 105.000
65
+ temp3_crit_hyst: -273.150
66
+ temp3_emergency: 110.000
67
+PPT:
68
+ power1_average: 29.000
69
+ power1_cap: 120.000
70
+acpitz-acpi-0
71
+temp1:
72
+ temp1_input: 88.000
src/go/collectors/go.d.plugin/modules/sensors/testdata/sensors-temp.txt
new
+81
@@ -0,0 +1,81 @@
1
+k10temp-pci-00cb
2
+Tctl:
3
+ temp1_input: 57.500
4
+Tccd1:
5
+ temp3_input: 54.000
6
+Tccd2:
7
+ temp4_input: 55.500
8
+Tccd3:
9
+ temp5_input: 56.000
10
+Tccd4:
11
+ temp6_input: 52.750
12
+Tccd5:
13
+ temp7_input: 53.500
14
+Tccd6:
15
+ temp8_input: 55.250
16
+Tccd7:
17
+ temp9_input: 53.000
18
+Tccd8:
19
+ temp10_input: 53.750
20
+
21
+bnxt_en-pci-6201
22
+temp1:
23
+ temp1_input: 81.000
24
+
25
+nvme-pci-6100
26
+Composite:
27
+ temp1_input: 48.850
28
+ temp1_max: 89.850
29
+ temp1_min: -20.150
30
+ temp1_crit: 94.850
31
+ temp1_alarm: 0.000
32
+
33
+nvme-pci-0100
34
+Composite:
35
+ temp1_input: 39.850
36
+ temp1_max: 89.850
37
+ temp1_min: -20.150
38
+ temp1_crit: 94.850
39
+ temp1_alarm: 0.000
40
+
41
+nouveau-pci-4100
42
+temp1:
43
+ temp1_input: 51.000
44
+ temp1_max: 95.000
45
+ temp1_max_hyst: 3.000
46
+ temp1_crit: 105.000
47
+ temp1_crit_hyst: 5.000
48
+ temp1_emergency: 135.000
49
+ temp1_emergency_hyst: 5.000
50
+
51
+k10temp-pci-00c3
52
+Tctl:
53
+ temp1_input: 62.000
54
+Tccd1:
55
+ temp3_input: 58.250
56
+Tccd2:
57
+ temp4_input: 60.250
58
+Tccd3:
59
+ temp5_input: 57.000
60
+Tccd4:
61
+ temp6_input: 57.250
62
+Tccd5:
63
+ temp7_input: 57.750
64
+Tccd6:
65
+ temp8_input: 59.500
66
+Tccd7:
67
+ temp9_input: 58.500
68
+Tccd8:
69
+ temp10_input: 61.250
70
+
71
+bnxt_en-pci-6200
72
+temp1:
73
+ temp1_input: 80.000
74
+
75
+nvme-pci-8100
76
+Composite:
77
+ temp1_input: 39.850
78
+ temp1_max: 89.850
79
+ temp1_min: -20.150
80
+ temp1_crit: 94.850
81
+ temp1_alarm: 0.000
src/go/collectors/go.d.plugin/modules/zfspool/zfspool_test.go
-2
@@ -67,7 +67,6 @@ func TestZFSPool_Init(t *testing.T) {
67
}
68
})
69
}
70
-
70
}
71
72
func TestZFSPool_Cleanup(t *testing.T) {
@@ -205,7 +204,6 @@ func TestZFSPool_Collect(t *testing.T) {
204
}
205
})
206
}
208
-
207
}
208
209
func prepareMockOK() *mockZpoolCLIExec {