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