@cryptotaxi247 / netdata-1 / commits / b7c4d442c

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 {