@cryptotaxi247 / netdata-1 / commits / 4fef11d5e

w1sensor collector Go implementation (#18464)

Co-authored-by: ilyam8 <ilya@netdata.cloud>

Fotis Voutsas committed Sep 4, 2024 at 11:05 UTC 4fef11d5ef5554d9cc868250f0dfef5307c7fc61
16 files changed +586
src/go/plugin/go.d/README.md
+1
@@ -151,6 +151,7 @@ see the appropriate collector readme.
151 | [vcsa](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/vcsa) | vCenter Server Appliance |
152 | [vernemq](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/vernemq) | VerneMQ |
153 | [vsphere](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/vsphere) | VMware vCenter Server |
154 +| [w1sensor](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/w1sensor) | 1-Wire Sensors |
155 | [web_log](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/weblog) | Apache/NGINX |
156 | [wireguard](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/wireguard) | WireGuard |
157 | [whoisquery](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/whoisquery) | Domain Expiry |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -116,6 +116,7 @@ modules:
116 # vernemq: yes
117 # vcsa: yes
118 # vsphere: yes
119 +# w1sensor: yes
120 # web_log: yes
121 # wireguard: yes
122 # whoisquery: yes
src/go/plugin/go.d/config/go.d/w1sensor.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/plugin/go.d/modules/w1sensor#readme
3 +
4 +jobs:
5 + - name: w1sensor
6 + sensors_path: /sys/bus/w1/devices
src/go/plugin/go.d/modules/init.go
+1
@@ -108,6 +108,7 @@ import (
108 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vcsa"
109 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vernemq"
110 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vsphere"
111 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/w1sensor"
112 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/weblog"
113 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/whoisquery"
114 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/windows"
src/go/plugin/go.d/modules/w1sensor/charts.go new
+57
@@ -0,0 +1,57 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package w1sensor
4 +
5 +import (
6 + "fmt"
7 + "strings"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 +)
11 +
12 +const (
13 + prioTemperature = module.Priority + iota
14 +)
15 +
16 +var (
17 + sensorChartTmpl = module.Chart{
18 + ID: "w1sensor_%s_temperature",
19 + Title: "1-Wire Temperature Sensor",
20 + Units: "Celsius",
21 + Fam: "Temperature",
22 + Ctx: "w1sensor.temperature",
23 + Type: module.Line,
24 + Priority: prioTemperature,
25 + Dims: module.Dims{
26 + {ID: "w1sensor_%s_temperature", Div: precision},
27 + },
28 + }
29 +)
30 +
31 +func (w *W1sensor) addSensorChart(id string) {
32 + chart := sensorChartTmpl.Copy()
33 +
34 + chart.ID = fmt.Sprintf(chart.ID, id)
35 + chart.Labels = []module.Label{
36 + {Key: "sensor_id", Value: id},
37 + }
38 +
39 + for _, dim := range chart.Dims {
40 + dim.ID = fmt.Sprintf(dim.ID, id)
41 + }
42 +
43 + if err := w.Charts().Add(chart); err != nil {
44 + w.Warning(err)
45 + }
46 +
47 +}
48 +
49 +func (w *W1sensor) removeSensorChart(id string) {
50 + px := fmt.Sprintf("w1sensor_%s", id)
51 + for _, chart := range *w.Charts() {
52 + if strings.HasPrefix(chart.ID, px) {
53 + chart.MarkRemove()
54 + chart.MarkNotCreated()
55 + }
56 + }
57 +}
src/go/plugin/go.d/modules/w1sensor/collect.go new
+110
@@ -0,0 +1,110 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package w1sensor
4 +
5 +import (
6 + "bufio"
7 + "errors"
8 + "fmt"
9 + "io/fs"
10 + "os"
11 + "path/filepath"
12 + "strconv"
13 + "strings"
14 +)
15 +
16 +const precision = 10
17 +
18 +func (w *W1sensor) collect() (map[string]int64, error) {
19 + des, err := os.ReadDir(w.SensorsPath)
20 + if err != nil {
21 + return nil, err
22 + }
23 +
24 + mx := make(map[string]int64)
25 + seen := make(map[string]bool)
26 +
27 + for _, de := range des {
28 + if !de.IsDir() {
29 + continue
30 + }
31 + if !isW1sensorDir(de.Name()) {
32 + w.Debugf("'%s' is not a w1sensor directory, skipping it", filepath.Join(w.SensorsPath, de.Name()))
33 + continue
34 + }
35 +
36 + filename := filepath.Join(w.SensorsPath, de.Name(), "w1_slave")
37 +
38 + temp, err := readW1sensorTemperature(filename)
39 + if err != nil {
40 + if errors.Is(err, fs.ErrNotExist) {
41 + w.Debugf("'%s' doesn't have 'w1_slave', skipping it", filepath.Join(w.SensorsPath, de.Name()))
42 + continue
43 + }
44 + return nil, fmt.Errorf("failed to read temperature from '%s': %w", filename, err)
45 + }
46 +
47 + seen[de.Name()] = true
48 + if !w.seenSensors[de.Name()] {
49 + w.addSensorChart(de.Name())
50 +
51 + }
52 +
53 + mx[fmt.Sprintf("w1sensor_%s_temperature", de.Name())] = temp
54 + }
55 +
56 + for id := range w.seenSensors {
57 + if !seen[id] {
58 + delete(w.seenSensors, id)
59 + w.removeSensorChart(id)
60 + }
61 + }
62 +
63 + if len(mx) == 0 {
64 + return nil, errors.New("no w1 sensors found")
65 + }
66 +
67 + return mx, nil
68 +}
69 +
70 +func readW1sensorTemperature(filename string) (int64, error) {
71 + file, err := os.Open(filename)
72 + if err != nil {
73 + return 0, err
74 + }
75 + defer file.Close()
76 +
77 + sc := bufio.NewScanner(file)
78 + sc.Scan()
79 + // The second line displays the retained values along with a temperature in milli degrees Centigrade after t=.
80 + sc.Scan()
81 +
82 + _, tempStr, ok := strings.Cut(strings.TrimSpace(sc.Text()), "t=")
83 + if !ok {
84 + return 0, errors.New("no temperature found")
85 + }
86 +
87 + v, err := strconv.ParseInt(tempStr, 10, 64)
88 + if err != nil {
89 + return 0, err
90 + }
91 +
92 + return int64(float64(v) / 1000 * precision), nil
93 +}
94 +
95 +func isW1sensorDir(dirName string) bool {
96 + // Supported family members
97 + // Based on linux/drivers/w1/w1_family.h and w1/slaves/w1_therm.c
98 + for _, px := range []string{
99 + "10-", // W1_THERM_DS18S20
100 + "22-", // W1_THERM_DS1822
101 + "28-", // W1_THERM_DS18B20
102 + "3b-", // W1_THERM_DS1825
103 + "42-", // W1_THERM_DS28EA00
104 + } {
105 + if strings.HasPrefix(dirName, px) {
106 + return true
107 + }
108 + }
109 + return false
110 +}
src/go/plugin/go.d/modules/w1sensor/config_schema.json new
+32
@@ -0,0 +1,32 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "Access Point 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": 1
13 + },
14 + "sensors_path": {
15 + "title": "Sensors path",
16 + "description": "Directory path containing sensor folders with w1_slave files.",
17 + "type": "string",
18 + "default": "/sys/bus/w1/devices"
19 + }
20 + },
21 + "required": [],
22 + "additionalProperties": false,
23 + "patternProperties": {
24 + "^name$": {}
25 + }
26 + },
27 + "uiSchema": {
28 + "uiOptions": {
29 + "fullPage": true
30 + }
31 + }
32 +}
src/go/plugin/go.d/modules/w1sensor/metadata.yaml new
+95
@@ -0,0 +1,95 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + plugin_name: go.d.plugin
5 + module_name: w1sensor
6 + monitored_instance:
7 + name: 1-Wire Sensors
8 + link: "https://www.analog.com/en/product-category/1wire-temperature-sensors.html"
9 + categories:
10 + - data-collection.hardware-devices-and-sensors
11 + icon_filename: "1-wire.png"
12 + related_resources:
13 + integrations:
14 + list: []
15 + info_provided_to_referring_integrations:
16 + description: ""
17 + keywords:
18 + - temperature
19 + - sensor
20 + - 1-wire
21 + most_popular: false
22 + overview:
23 + data_collection:
24 + metrics_description: |
25 + Monitor 1-Wire Sensors metrics with Netdata for optimal environmental conditions monitoring. Enhance your environmental monitoring with real-time insights and alerts.
26 + method_description: The collector uses the wire, w1_gpio, and w1_therm kernel modules. Currently temperature sensors are supported and automatically detected.
27 + supported_platforms:
28 + include:
29 + - Linux
30 + exclude: []
31 + multi_instance: true
32 + additional_permissions:
33 + description: ""
34 + default_behavior:
35 + auto_detection:
36 + description: "The collector will try to auto detect available 1-Wire devices."
37 + limits:
38 + description: ""
39 + performance_impact:
40 + description: ""
41 + setup:
42 + prerequisites:
43 + list:
44 + - title: "Required Linux kernel modules"
45 + description: "Make sure `wire`, `w1_gpio`, and `w1_therm` kernel modules are loaded."
46 + configuration:
47 + file:
48 + name: go.d/w1sensor.conf
49 + options:
50 + description: |
51 + The following options can be defined globally: update_every.
52 + folding:
53 + title: Config options
54 + enabled: true
55 + list:
56 + - name: update_every
57 + description: Data collection frequency.
58 + default_value: 1
59 + required: false
60 + - name: sensors_path
61 + description: Directory path containing sensor folders with w1_slave files.
62 + default_value: /sys/bus/w1/devices
63 + required: false
64 + examples:
65 + folding:
66 + title: ""
67 + enabled: false
68 + list:
69 + - name: Custom sensor device path
70 + description: Monitors a virtual sensor when the w1_slave file is located in a custom directory instead of the default location.
71 + config: |
72 + jobs:
73 + - name: custom_sensors_path
74 + sensors_path: /custom/path/devices
75 + troubleshooting:
76 + problems:
77 + list: []
78 + alerts: []
79 + metrics:
80 + folding:
81 + title: Metrics
82 + enabled: false
83 + description: ""
84 + availability: []
85 + scopes:
86 + - name: sensor
87 + description: These metrics refer to the 1-Wire Sensor.
88 + labels: []
89 + metrics:
90 + - name: w1sensor.temperature
91 + description: 1-Wire Temperature Sensor
92 + unit: "Celsius"
93 + chart_type: line
94 + dimensions:
95 + - name: temperature
src/go/plugin/go.d/modules/w1sensor/testdata/config.json new
+4
@@ -0,0 +1,4 @@
1 +{
2 + "update_every": 123,
3 + "sensors_path": "ok"
4 +}
src/go/plugin/go.d/modules/w1sensor/testdata/config.yaml new
+2
@@ -0,0 +1,2 @@
1 +update_every: 123
2 +sensors_path: "ok"
src/go/plugin/go.d/modules/w1sensor/testdata/devices/28-01204e9d2fa0/w1_slave new
+2
@@ -0,0 +1,2 @@
1 +17 01 4b 46 7f ff 0c 10 71 : crc=71 YES
2 +17 01 4b 46 7f ff 0c 10 71 t=12435
\ No newline at end of file
src/go/plugin/go.d/modules/w1sensor/testdata/devices/28-01204e9d2fa1/w1_slave new
+2
@@ -0,0 +1,2 @@
1 +17 01 4b 46 7f ff 0c 10 71 : crc=71 YES
2 +17 01 4b 46 7f ff 0c 10 71 t=29960
\ No newline at end of file
src/go/plugin/go.d/modules/w1sensor/testdata/devices/28-01204e9d2fa2/w1_slave new
+2
@@ -0,0 +1,2 @@
1 +17 01 4b 46 7f ff 0c 10 71 : crc=71 YES
2 +17 01 4b 46 7f ff 0c 10 71 t=10762
\ No newline at end of file
src/go/plugin/go.d/modules/w1sensor/testdata/devices/28-01204e9d2fa3/w1_slave new
+2
@@ -0,0 +1,2 @@
1 +17 01 4b 46 7f ff 0c 10 71 : crc=71 YES
2 +17 01 4b 46 7f ff 0c 10 71 t=22926
\ No newline at end of file
src/go/plugin/go.d/modules/w1sensor/w1sensor.go new
+96
@@ -0,0 +1,96 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package w1sensor
4 +
5 +import (
6 + _ "embed"
7 + "errors"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 +)
11 +
12 +//go:embed "config_schema.json"
13 +var configSchema string
14 +
15 +func init() {
16 + module.Register("w1sensor", module.Creator{
17 + JobConfigSchema: configSchema,
18 + Defaults: module.Defaults{
19 + UpdateEvery: 1,
20 + },
21 + Create: func() module.Module { return New() },
22 + Config: func() any { return &Config{} },
23 + })
24 +}
25 +
26 +func New() *W1sensor {
27 + return &W1sensor{
28 + Config: Config{
29 + SensorsPath: "/sys/bus/w1/devices",
30 + },
31 + charts: &module.Charts{},
32 + seenSensors: make(map[string]bool),
33 + }
34 +}
35 +
36 +type Config struct {
37 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38 + SensorsPath string `yaml:"sensors_path,omitempty" json:"sensors_path"`
39 +}
40 +
41 +type (
42 + W1sensor struct {
43 + module.Base
44 + Config `yaml:",inline" json:""`
45 +
46 + charts *module.Charts
47 +
48 + seenSensors map[string]bool
49 + }
50 +)
51 +
52 +func (w *W1sensor) Configuration() any {
53 + return w.Config
54 +}
55 +
56 +func (w *W1sensor) Init() error {
57 + if w.SensorsPath == "" {
58 + w.Errorf("sensors_path required but not set")
59 + return errors.New("no sensors path specified")
60 + }
61 +
62 + return nil
63 +}
64 +
65 +func (w *W1sensor) Check() error {
66 + mx, err := w.collect()
67 + if err != nil {
68 + w.Error(err)
69 + return err
70 + }
71 +
72 + if len(mx) == 0 {
73 + return errors.New("no metrics collected")
74 + }
75 +
76 + return nil
77 +}
78 +
79 +func (w *W1sensor) Charts() *module.Charts {
80 + return w.charts
81 +}
82 +
83 +func (w *W1sensor) Collect() map[string]int64 {
84 + mx, err := w.collect()
85 + if err != nil {
86 + w.Error(err)
87 + }
88 +
89 + if len(mx) == 0 {
90 + return nil
91 + }
92 +
93 + return mx
94 +}
95 +
96 +func (w *W1sensor) Cleanup() {}
src/go/plugin/go.d/modules/w1sensor/w1sensor_test.go new
+173
@@ -0,0 +1,173 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package w1sensor
4 +
5 +import (
6 + "os"
7 + "testing"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 +
11 + "github.com/stretchr/testify/assert"
12 + "github.com/stretchr/testify/require"
13 +)
14 +
15 +var (
16 + dataConfigJSON, _ = os.ReadFile("testdata/config.json")
17 + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
18 +)
19 +
20 +func Test_testDataIsValid(t *testing.T) {
21 + for name, data := range map[string][]byte{
22 + "dataConfigJSON": dataConfigJSON,
23 + "dataConfigYAML": dataConfigYAML,
24 + } {
25 + require.NotNil(t, data, name)
26 + }
27 +}
28 +
29 +func TestW1sensor_Configuration(t *testing.T) {
30 + module.TestConfigurationSerialize(t, &W1sensor{}, dataConfigJSON, dataConfigYAML)
31 +}
32 +
33 +func TestW1sensor_Init(t *testing.T) {
34 + tests := map[string]struct {
35 + config Config
36 + wantFail bool
37 + }{
38 + "fails if 'sensors_path' is not set": {
39 + wantFail: true,
40 + config: Config{
41 + SensorsPath: "",
42 + },
43 + },
44 + }
45 +
46 + for name, test := range tests {
47 + t.Run(name, func(t *testing.T) {
48 + w1 := New()
49 + w1.Config = test.config
50 +
51 + if test.wantFail {
52 + assert.Error(t, w1.Init())
53 + } else {
54 + assert.NoError(t, w1.Init())
55 + }
56 + })
57 + }
58 +}
59 +
60 +func TestAP_Cleanup(t *testing.T) {
61 + tests := map[string]struct {
62 + prepare func() *W1sensor
63 + }{
64 + "not initialized exec": {
65 + prepare: func() *W1sensor {
66 + return New()
67 + },
68 + },
69 + "after check": {
70 + prepare: func() *W1sensor {
71 + w1 := prepareCaseOk()
72 + _ = w1.Check()
73 + return w1
74 + },
75 + },
76 + "after collect": {
77 + prepare: func() *W1sensor {
78 + w1 := prepareCaseOk()
79 + _ = w1.Collect()
80 + return w1
81 + },
82 + },
83 + }
84 +
85 + for name, test := range tests {
86 + t.Run(name, func(t *testing.T) {
87 + w1 := test.prepare()
88 +
89 + assert.NotPanics(t, w1.Cleanup)
90 + })
91 + }
92 +}
93 +
94 +func TestW1sensor_Charts(t *testing.T) {
95 + assert.NotNil(t, New().Charts())
96 +}
97 +
98 +func TestW1sensor_Check(t *testing.T) {
99 + tests := map[string]struct {
100 + prepareMock func() *W1sensor
101 + wantFail bool
102 + }{
103 + "success case": {
104 + wantFail: false,
105 + prepareMock: prepareCaseOk,
106 + },
107 + "no sensors dir": {
108 + wantFail: true,
109 + prepareMock: prepareCaseNoSensorsDir,
110 + },
111 + }
112 +
113 + for name, test := range tests {
114 + t.Run(name, func(t *testing.T) {
115 + w1 := test.prepareMock()
116 +
117 + if test.wantFail {
118 + assert.Error(t, w1.Check())
119 + } else {
120 + assert.NoError(t, w1.Check())
121 + }
122 + })
123 + }
124 +}
125 +
126 +func TestW1Sensors_Collect(t *testing.T) {
127 + tests := map[string]struct {
128 + prepareMock func() *W1sensor
129 + wantMetrics map[string]int64
130 + wantCharts int
131 + }{
132 + "success case": {
133 + prepareMock: prepareCaseOk,
134 + wantCharts: 4,
135 + wantMetrics: map[string]int64{
136 + "w1sensor_28-01204e9d2fa0_temperature": 124,
137 + "w1sensor_28-01204e9d2fa1_temperature": 299,
138 + "w1sensor_28-01204e9d2fa2_temperature": 107,
139 + "w1sensor_28-01204e9d2fa3_temperature": 229,
140 + },
141 + },
142 + "no sensors dir": {
143 + prepareMock: prepareCaseNoSensorsDir,
144 + wantMetrics: nil,
145 + },
146 + }
147 +
148 + for name, test := range tests {
149 + t.Run(name, func(t *testing.T) {
150 + w1 := test.prepareMock()
151 +
152 + mx := w1.Collect()
153 +
154 + assert.Equal(t, test.wantMetrics, mx)
155 +
156 + assert.Equal(t, test.wantCharts, len(*w1.Charts()), "wantCharts")
157 +
158 + module.TestMetricsHasAllChartsDims(t, w1.Charts(), mx)
159 + })
160 + }
161 +}
162 +
163 +func prepareCaseOk() *W1sensor {
164 + w1 := New()
165 + w1.SensorsPath = "testdata/devices"
166 + return w1
167 +}
168 +
169 +func prepareCaseNoSensorsDir() *W1sensor {
170 + w1 := New()
171 + w1.SensorsPath = "testdata/devices!"
172 + return w1
173 +}