summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIlya Mashchenko <ilya@netdata.cloud>2024-04-22 11:32:47 +0300
committerGitHub <noreply@github.com>2024-04-22 11:32:47 +0300
commitb7c4d442c5e0f61ba5eacd042d921955b3e763b3 (patch)
tree0e0e0d216f509bf889adb7992f9696fc7867e5fe
parent827fe2afcf761380c465b7878eced4a0f3a12a6e (diff)
go.d add sensors (#17466)
-rw-r--r--src/go/collectors/go.d.plugin/README.md2
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d.conf1
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d/sensors.conf6
-rw-r--r--src/go/collectors/go.d.plugin/modules/init.go1
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/charts.go159
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/collect.go179
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/config_schema.json47
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/exec.go41
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/init.go38
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/metadata.yaml157
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/sensors.go111
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/sensors_test.go308
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/testdata/config.json5
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/testdata/config.yaml3
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/testdata/sensors-temp-in-curr-power-fan.txt72
-rw-r--r--src/go/collectors/go.d.plugin/modules/sensors/testdata/sensors-temp.txt81
-rw-r--r--src/go/collectors/go.d.plugin/modules/zfspool/zfspool_test.go2
17 files changed, 1211 insertions, 2 deletions
diff --git a/src/go/collectors/go.d.plugin/README.md b/src/go/collectors/go.d.plugin/README.md
index fc688ada01..b7f35a1e54 100644
--- a/src/go/collectors/go.d.plugin/README.md
+++ b/src/go/collectors/go.d.plugin/README.md
@@ -76,6 +76,7 @@ see the appropriate collector readme.
| [fluentd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fluentd) | Fluentd |
| [freeradius](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/freeradius) | FreeRADIUS |
| [haproxy](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/haproxy) | HAProxy |
+| [hddtemp](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hddtemp) | Disks temperature |
| [hdfs](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hdfs) | HDFS |
| [httpcheck](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/httpcheck) | Any HTTP Endpoint |
| [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.
| [rabbitmq](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/rabbitmq) | RabbitMQ |
| [redis](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/redis) | Redis |
| [scaleio](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/scaleio) | Dell EMC ScaleIO |
+| [sensors](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules) | Hardware Sensors |
| [SNMP](https://github.com/netdata/netdata/blob/master/src/go/collectors/go.d.plugin/modules/snmp) | SNMP |
| [squidlog](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/squidlog) | Squid |
| [storcli](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/storcli) | Broadcom Hardware RAID |
diff --git a/src/go/collectors/go.d.plugin/config/go.d.conf b/src/go/collectors/go.d.plugin/config/go.d.conf
index 9fe91db5d9..928cb15a06 100644
--- a/src/go/collectors/go.d.plugin/config/go.d.conf
+++ b/src/go/collectors/go.d.plugin/config/go.d.conf
@@ -76,6 +76,7 @@ modules:
# rabbitmq: yes
# redis: yes
# scaleio: yes
+# sensors: yes
# snmp: yes
# squidlog: yes
# storcli: yes
diff --git a/src/go/collectors/go.d.plugin/config/go.d/sensors.conf b/src/go/collectors/go.d.plugin/config/go.d/sensors.conf
new file mode 100644
index 0000000000..3b8febde89
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/config/go.d/sensors.conf
@@ -0,0 +1,6 @@
+## All available configuration options, their descriptions and default values:
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/sensors#readme
+
+jobs:
+ - name: sensors
+ binary_path: /usr/bin/sensors
diff --git a/src/go/collectors/go.d.plugin/modules/init.go b/src/go/collectors/go.d.plugin/modules/init.go
index 90f1102914..8691ca024d 100644
--- a/src/go/collectors/go.d.plugin/modules/init.go
+++ b/src/go/collectors/go.d.plugin/modules/init.go
@@ -68,6 +68,7 @@ import (
_ "github.com/netdata/netdata/go/go.d.plugin/modules/rabbitmq"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/redis"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/scaleio"
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/sensors"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/snmp"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/squidlog"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/storcli"
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/charts.go b/src/go/collectors/go.d.plugin/modules/sensors/charts.go
new file mode 100644
index 0000000000..20df057c88
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/charts.go
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioSensorTemperature = module.Priority + iota
+ prioSensorVoltage
+ prioSensorCurrent
+ prioSensorPower
+ prioSensorFan
+ prioSensorEnergy
+ prioSensorHumidity
+)
+
+var sensorTemperatureChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_temperature",
+ Title: "Sensor temperature",
+ Units: "Celsius",
+ Fam: "temperature",
+ Ctx: "sensors.sensor_temperature",
+ Type: module.Line,
+ Priority: prioSensorTemperature,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "temperature", Div: precision},
+ },
+}
+
+var sensorVoltageChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_voltage",
+ Title: "Sensor voltage",
+ Units: "Volts",
+ Fam: "voltage",
+ Ctx: "sensors.sensor_voltage",
+ Type: module.Line,
+ Priority: prioSensorVoltage,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "voltage", Div: precision},
+ },
+}
+
+var sensorCurrentChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_current",
+ Title: "Sensor current",
+ Units: "Amperes",
+ Fam: "current",
+ Ctx: "sensors.sensor_current",
+ Type: module.Line,
+ Priority: prioSensorCurrent,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "current", Div: precision},
+ },
+}
+
+var sensorPowerChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_power",
+ Title: "Sensor power",
+ Units: "Watts",
+ Fam: "power",
+ Ctx: "sensors.sensor_power",
+ Type: module.Line,
+ Priority: prioSensorPower,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "power", Div: precision},
+ },
+}
+
+var sensorFanChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_fan",
+ Title: "Sensor fan speed",
+ Units: "RPM",
+ Fam: "fan",
+ Ctx: "sensors.sensor_fan_speed",
+ Type: module.Line,
+ Priority: prioSensorFan,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "fan", Div: precision},
+ },
+}
+
+var sensorEnergyChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_energy",
+ Title: "Sensor energy",
+ Units: "Joules",
+ Fam: "energy",
+ Ctx: "sensors.sensor_energy",
+ Type: module.Line,
+ Priority: prioSensorEnergy,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "energy", Div: precision},
+ },
+}
+
+var sensorHumidityChartTmpl = module.Chart{
+ ID: "sensor_chip_%s_feature_%s_subfeature_%s_humidity",
+ Title: "Sensor humidity",
+ Units: "percent",
+ Fam: "humidity",
+ Ctx: "sensors.sensor_humidity",
+ Type: module.Area,
+ Priority: prioSensorHumidity,
+ Dims: module.Dims{
+ {ID: "sensor_chip_%s_feature_%s_subfeature_%s", Name: "humidity", Div: precision},
+ },
+}
+
+func (s *Sensors) addSensorChart(sn sensorStats) {
+ var chart *module.Chart
+
+ switch sensorType(sn) {
+ case sensorTypeTemp:
+ chart = sensorTemperatureChartTmpl.Copy()
+ case sensorTypeVoltage:
+ chart = sensorVoltageChartTmpl.Copy()
+ case sensorTypePower:
+ chart = sensorPowerChartTmpl.Copy()
+ case sensorTypeHumidity:
+ chart = sensorHumidityChartTmpl.Copy()
+ case sensorTypeFan:
+ chart = sensorFanChartTmpl.Copy()
+ case sensorTypeCurrent:
+ chart = sensorCurrentChartTmpl.Copy()
+ case sensorTypeEnergy:
+ chart = sensorEnergyChartTmpl.Copy()
+ default:
+ return
+ }
+
+ chip, feat, subfeat := snakeCase(sn.chip), snakeCase(sn.feature), snakeCase(sn.subfeature)
+
+ chart.ID = fmt.Sprintf(chart.ID, chip, feat, subfeat)
+ chart.Labels = []module.Label{
+ {Key: "chip", Value: sn.chip},
+ {Key: "feature", Value: sn.feature},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, chip, feat, subfeat)
+ }
+
+ if err := s.Charts().Add(chart); err != nil {
+ s.Warning(err)
+ }
+}
+
+func (s *Sensors) removeSensorChart(px string) {
+ for _, chart := range *s.Charts() {
+ if strings.HasPrefix(chart.ID, px) {
+ chart.MarkRemove()
+ chart.MarkNotCreated()
+ return
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/collect.go b/src/go/collectors/go.d.plugin/modules/sensors/collect.go
new file mode 100644
index 0000000000..46e900ad0a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/collect.go
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type sensorStats struct {
+ chip string
+ feature string
+ subfeature string
+ value string
+}
+
+func (s *sensorStats) String() string {
+ return fmt.Sprintf("chip:%s feat:%s subfeat:%s value:%s", s.chip, s.feature, s.subfeature, s.value)
+}
+
+const (
+ sensorTypeTemp = "temperature"
+ sensorTypeVoltage = "voltage"
+ sensorTypePower = "power"
+ sensorTypeHumidity = "humidity"
+ sensorTypeFan = "fan"
+ sensorTypeCurrent = "current"
+ sensorTypeEnergy = "energy"
+)
+
+const precision = 1000
+
+func (s *Sensors) collect() (map[string]int64, error) {
+ bs, err := s.exec.sensorsInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ if len(bs) == 0 {
+ return nil, errors.New("empty response from sensors")
+ }
+
+ sensors, err := parseSensors(bs)
+ if err != nil {
+ return nil, err
+ }
+ if len(sensors) == 0 {
+ return nil, errors.New("no sensors found")
+ }
+
+ mx := make(map[string]int64)
+ seen := make(map[string]bool)
+
+ for _, sn := range sensors {
+ // TODO: Most likely we need different values depending on the type of sensor.
+ if !strings.HasSuffix(sn.subfeature, "_input") {
+ s.Debugf("skipping non input sensor: '%s'", sn)
+ continue
+ }
+
+ v, err := strconv.ParseFloat(sn.value, 64)
+ if err != nil {
+ s.Debugf("parsing value for sensor '%s': %v", sn, err)
+ continue
+ }
+
+ if sensorType(sn) == "" {
+ s.Debugf("can not find type for sensor '%s'", sn)
+ continue
+ }
+
+ if minVal, maxVal, ok := sensorLimits(sn); ok && (v < minVal || v > maxVal) {
+ s.Debugf("value outside limits [%d/%d] for sensor '%s'", int64(minVal), int64(maxVal), sn)
+ continue
+ }
+
+ key := fmt.Sprintf("sensor_chip_%s_feature_%s_subfeature_%s", sn.chip, sn.feature, sn.subfeature)
+ key = snakeCase(key)
+ if !s.sensors[key] {
+ s.sensors[key] = true
+ s.addSensorChart(sn)
+ }
+
+ seen[key] = true
+
+ mx[key] = int64(v * precision)
+ }
+
+ for k := range s.sensors {
+ if !seen[k] {
+ delete(s.sensors, k)
+ s.removeSensorChart(k)
+ }
+ }
+
+ return mx, nil
+}
+
+func snakeCase(n string) string {
+ return strings.ToLower(strings.ReplaceAll(n, " ", "_"))
+}
+
+func sensorLimits(sn sensorStats) (minVal float64, maxVal float64, ok bool) {
+ switch sensorType(sn) {
+ case sensorTypeTemp:
+ return -127, 1000, true
+ case sensorTypeVoltage:
+ return -400, 400, true
+ case sensorTypeCurrent:
+ return -127, 127, true
+ case sensorTypeFan:
+ return 0, 65535, true
+ default:
+ return 0, 0, false
+ }
+}
+
+func sensorType(sn sensorStats) string {
+ switch {
+ case strings.HasPrefix(sn.subfeature, "temp"):
+ return sensorTypeTemp
+ case strings.HasPrefix(sn.subfeature, "in"):
+ return sensorTypeVoltage
+ case strings.HasPrefix(sn.subfeature, "power"):
+ return sensorTypePower
+ case strings.HasPrefix(sn.subfeature, "humidity"):
+ return sensorTypeHumidity
+ case strings.HasPrefix(sn.subfeature, "fan"):
+ return sensorTypeFan
+ case strings.HasPrefix(sn.subfeature, "curr"):
+ return sensorTypeCurrent
+ case strings.HasPrefix(sn.subfeature, "energy"):
+ return sensorTypeEnergy
+ default:
+ return ""
+ }
+}
+
+func parseSensors(output []byte) ([]sensorStats, error) {
+ var sensors []sensorStats
+
+ sc := bufio.NewScanner(bytes.NewReader(output))
+
+ var chip, feat string
+
+ for sc.Scan() {
+ text := sc.Text()
+ if text == "" {
+ chip, feat = "", ""
+ continue
+ }
+
+ switch {
+ case strings.HasPrefix(text, " ") && chip != "" && feat != "":
+ parts := strings.Split(text, ":")
+ if len(parts) != 2 {
+ continue
+ }
+ subfeat, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
+ sensors = append(sensors, sensorStats{
+ chip: chip,
+ feature: feat,
+ subfeature: subfeat,
+ value: value,
+ })
+ case strings.HasSuffix(text, ":") && chip != "":
+ feat = strings.TrimSpace(strings.TrimSuffix(text, ":"))
+ default:
+ chip = text
+ feat = ""
+ }
+ }
+
+ return sensors, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/config_schema.json b/src/go/collectors/go.d.plugin/modules/sensors/config_schema.json
new file mode 100644
index 0000000000..6c12ca9b8d
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/config_schema.json
@@ -0,0 +1,47 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Sensors collector configuration",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 10
+ },
+ "binary_path": {
+ "title": "Binary path",
+ "description": "Path to the `sensors` binary.",
+ "type": "string",
+ "default": "/usr/bin/sensors"
+ },
+ "timeout": {
+ "title": "Timeout",
+ "description": "Timeout for executing the binary, specified in seconds.",
+ "type": "number",
+ "minimum": 0.5,
+ "default": 2
+ }
+ },
+ "required": [
+ "binary_path"
+ ],
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "binary_path": {
+ "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."
+ },
+ "timeout": {
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/exec.go b/src/go/collectors/go.d.plugin/modules/sensors/exec.go
new file mode 100644
index 0000000000..b920da66ec
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/exec.go
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+)
+
+func newSensorsCliExec(binPath string, timeout time.Duration) *sensorsCliExec {
+ return &sensorsCliExec{
+ binPath: binPath,
+ timeout: timeout,
+ }
+}
+
+type sensorsCliExec struct {
+ *logger.Logger
+
+ binPath string
+ timeout time.Duration
+}
+
+func (e *sensorsCliExec) sensorsInfo() ([]byte, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, e.binPath, "-A", "-u")
+ e.Debugf("executing '%s'", cmd)
+
+ bs, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
+ }
+
+ return bs, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/init.go b/src/go/collectors/go.d.plugin/modules/sensors/init.go
new file mode 100644
index 0000000000..6753693da5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/init.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+func (s *Sensors) validateConfig() error {
+ if s.BinaryPath == "" {
+ return errors.New("no sensors binary path specified")
+ }
+ return nil
+}
+
+func (s *Sensors) initSensorsCliExec() (sensorsCLI, error) {
+ binPath := s.BinaryPath
+
+ if !strings.HasPrefix(binPath, "/") {
+ path, err := exec.LookPath(binPath)
+ if err != nil {
+ return nil, err
+ }
+ binPath = path
+ }
+
+ if _, err := os.Stat(binPath); err != nil {
+ return nil, err
+ }
+
+ sensorsExec := newSensorsCliExec(binPath, s.Timeout.Duration())
+ sensorsExec.Logger = s.Logger
+
+ return sensorsExec, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/metadata.yaml b/src/go/collectors/go.d.plugin/modules/sensors/metadata.yaml
new file mode 100644
index 0000000000..5ea94f3982
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/metadata.yaml
@@ -0,0 +1,157 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-sensors
+ plugin_name: go.d.plugin
+ module_name: sensors
+ monitored_instance:
+ name: Linux Sensors (lm-sensors)
+ link: https://hwmon.wiki.kernel.org/lm_sensors
+ icon_filename: "microchip.svg"
+ categories:
+ - data-collection.hardware-devices-and-sensors
+ keywords:
+ - sensors
+ - temperature
+ - voltage
+ - current
+ - power
+ - fan
+ - energy
+ - humidity
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: >
+ This collector gathers real-time system sensor statistics,
+ including temperature, voltage, current, power, fan speed, energy consumption, and humidity,
+ utilizing the [sensors](https://linux.die.net/man/1/sensors) binary.
+ method_description: ""
+ supported_platforms:
+ include: []
+ exclude: []
+ multi_instance: false
+ additional_permissions:
+ description: ""
+ default_behavior:
+ auto_detection:
+ description: |
+ The following type of sensors are auto-detected:
+
+ - temperature
+ - fan
+ - voltage
+ - current
+ - power
+ - energy
+ - humidity
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ setup:
+ prerequisites:
+ list:
+ - title: Install lm-sensors
+ description: |
+ - Install `lm-sensors` using your distribution's package manager.
+ - Run `sensors-detect` to detect hardware monitoring chips.
+ configuration:
+ file:
+ name: go.d/sensors.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 10
+ required: false
+ - name: binary_path
+ 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.
+ default_value: /usr/bin/sensors
+ required: true
+ - name: timeout
+ description: Timeout for executing the binary, specified in seconds.
+ default_value: 2
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list:
+ - name: Custom binary path
+ description: The executable is not in the directories specified in the PATH environment variable.
+ config: |
+ jobs:
+ - name: sensors
+ binary_path: /usr/local/sbin/sensors
+ troubleshooting:
+ problems:
+ list: []
+ alerts: []
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: sensor
+ description: These metrics refer to the sensor.
+ labels:
+ - name: chip
+ description: The hardware component responsible for the sensor monitoring.
+ - name: feature
+ description: The specific sensor or monitoring point provided by the chip.
+ metrics:
+ - name: sensors.sensor_temperature
+ description: Sensor temperature
+ unit: Celsius
+ chart_type: line
+ dimensions:
+ - name: temperature
+ - name: sensors.sensor_voltage
+ description: Sensor voltage
+ unit: Volts
+ chart_type: line
+ dimensions:
+ - name: voltage
+ - name: sensors.sensor_current
+ description: Sensor current
+ unit: Amperes
+ chart_type: line
+ dimensions:
+ - name: current
+ - name: sensors.sensor_power
+ description: Sensor power
+ unit: Watts
+ chart_type: line
+ dimensions:
+ - name: power
+ - name: sensors.sensor_fan_speed
+ description: Sensor fan speed
+ unit: RPM
+ chart_type: line
+ dimensions:
+ - name: fan
+ - name: sensors.sensor_energy
+ description: Sensor energy
+ unit: Joules
+ chart_type: line
+ dimensions:
+ - name: energy
+ - name: sensors.sensor_humidity
+ description: Sensor humidity
+ unit: percent
+ chart_type: area
+ dimensions:
+ - name: humidity
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/sensors.go b/src/go/collectors/go.d.plugin/modules/sensors/sensors.go
new file mode 100644
index 0000000000..f909811d7c
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/sensors.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("sensors", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 10,
+ },
+ Create: func() module.Module { return New() },
+ })
+}
+
+func New() *Sensors {
+ return &Sensors{
+ Config: Config{
+ BinaryPath: "/usr/bin/sensors",
+ Timeout: web.Duration(time.Second * 2),
+ },
+ charts: &module.Charts{},
+ sensors: make(map[string]bool),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
+ BinaryPath string `yaml:"binary_path" json:"binary_path"`
+}
+
+type (
+ Sensors struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ exec sensorsCLI
+
+ sensors map[string]bool
+ }
+ sensorsCLI interface {
+ sensorsInfo() ([]byte, error)
+ }
+)
+
+func (s *Sensors) Configuration() any {
+ return s.Config
+}
+
+func (s *Sensors) Init() error {
+ if err := s.validateConfig(); err != nil {
+ s.Errorf("config validation: %s", err)
+ return err
+ }
+
+ sensorsExec, err := s.initSensorsCliExec()
+ if err != nil {
+ s.Errorf("sensors exec initialization: %v", err)
+ return err
+ }
+ s.exec = sensorsExec
+
+ return nil
+}
+
+func (s *Sensors) Check() error {
+ mx, err := s.collect()
+ if err != nil {
+ s.Error(err)
+ return err
+ }
+
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+
+ return nil
+}
+
+func (s *Sensors) Charts() *module.Charts {
+ return s.charts
+}
+
+func (s *Sensors) Collect() map[string]int64 {
+ mx, err := s.collect()
+ if err != nil {
+ s.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (s *Sensors) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/sensors/sensors_test.go b/src/go/collectors/go.d.plugin/modules/sensors/sensors_test.go
new file mode 100644
index 0000000000..d9b4242e7b
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/sensors/sensors_test.go
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package sensors
+
+import (
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+
+ dataSensorsTemp, _ = os.ReadFile("testdata/sensors-temp.txt")
+ dataSensorsTempInCurrPowerFan, _ = os.ReadFile("testdata/sensors-temp-in-curr-power-fan.txt")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+
+ "dataSensorsTemp": dataSensorsTemp,
+ "dataSensorsTempInCurrPowerFan": dataSensorsTempInCurrPowerFan,
+ } {
+ require.NotNil(t, data, name)
+
+ }
+}
+
+func TestSensors_Configuration(t *testing.T) {
+ module.TestConfigurationSerialize(t, &Sensors{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestSensors_Init(t *testing.T) {
+ tests := map[string]struct {
+ config Config
+ wantFail bool
+ }{
+ "fails if 'binary_path' is not set": {
+ wantFail: true,
+ config: Config{
+ BinaryPath: "",
+ },
+ },
+ "fails if failed to find binary": {
+ wantFail: true,
+ config: Config{
+ BinaryPath: "sensors!!!",
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ sensors := New()
+ sensors.Config = test.config
+
+ if test.wantFail {
+ assert.Error(t, sensors.Init())
+ } else {
+ assert.NoError(t, sensors.Init())
+ }
+ })
+ }
+}
+
+func TestSensors_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *Sensors
+ }{
+ "not initialized exec": {
+ prepare: func() *Sensors {
+ return New()
+ },
+ },
+ "after check": {
+ prepare: func() *Sensors {
+ sensors := New()
+ sensors.exec = prepareMockOkOnlyTemp()
+ _ = sensors.Check()
+ return sensors
+ },
+ },
+ "after collect": {
+ prepare: func() *Sensors {
+ sensors := New()
+ sensors.exec = prepareMockOkTempInCurrPowerFan()
+ _ = sensors.Collect()
+ return sensors
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ sensors := test.prepare()
+
+ assert.NotPanics(t, sensors.Cleanup)
+ })
+ }
+}
+
+func TestSensors_Charts(t *testing.T) {
+ assert.NotNil(t, New().Charts())
+}
+
+func TestSensors_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockSensorsCL