Browse Source

go.d add sensors (#17466)

Ilya Mashchenko 10 months ago
parent
commit
b7c4d442c5

+ 2 - 0
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     |

+ 1 - 0
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

+ 6 - 0
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

+ 1 - 0
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"

+ 159 - 0
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
+		}
+	}
+}

+ 179 - 0
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
+}

+ 47 - 0
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)."
+    }
+  }
+}

+ 41 - 0
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
+}

+ 38 - 0
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
+}

+ 157 - 0
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

Some files were not shown because too many files changed in this diff