go.d add hddtemp (#17462)
Ilya Mashchenko committed
Apr 21, 2024 at 16:13 UTC
7826adcf6ce4a9678f6cfefcc2c5edc80ab9f57e
15 files changed
+882
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -39,6 +39,7 @@ modules:
39
# fluentd: yes
40
# freeradius: yes
41
# haproxy: yes
42
+# hddtemp: yes
43
# hdfs: yes
44
# httpcheck: yes
45
# intelgpu: yes
src/go/collectors/go.d.plugin/config/go.d/hddtemp.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/hddtemp#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# address: 127.0.0.1:7634
src/go/collectors/go.d.plugin/config/go.d/sd/net_listeners.conf
+7
@@ -50,6 +50,8 @@ classify:
50
expr: '{{ and (eq .Port "6060") (eq .Comm "geth") }}'
51
- tags: "haproxy"
52
expr: '{{ and (eq .Port "8404") (eq .Comm "haproxy") }}'
53
+ - tags: "hddtemp"
54
+ expr: '{{ and (eq .Port "7634") (eq .Comm "hddtemp") }}'
55
- tags: "hdfs_namenode"
56
expr: '{{ and (eq .Port "9870") (eq .Comm "hadoop") }}'
57
- tags: "hdfs_datanode"
@@ -226,6 +228,11 @@ compose:
228
module: haproxy
229
name: local
230
url: http://{{.Address}}/metrics
231
+ - selector: "hddtemp"
232
+ template: |
233
+ module: hddtemp
234
+ name: local
235
+ address: {{.Address}}
236
- selector: "hdfs_namenode"
237
template: |
238
module: hdfs
src/go/collectors/go.d.plugin/modules/hddtemp/charts.go
new
+70
@@ -0,0 +1,70 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package hddtemp
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
10
+)
11
+
12
+const (
13
+ prioDiskTemperature = module.Priority + iota
14
+ prioDiskTemperatureSensorStatus
15
+)
16
+
17
+var (
18
+ diskTemperatureChartsTmpl = module.Chart{
19
+ ID: "disk_%s_temperature",
20
+ Title: "Disk temperature",
21
+ Units: "Celsius",
22
+ Fam: "temperature",
23
+ Ctx: "hddtemp.disk_temperature",
24
+ Type: module.Line,
25
+ Priority: prioDiskTemperature,
26
+ Dims: module.Dims{
27
+ {ID: "disk_%s_temperature", Name: "temperature"},
28
+ },
29
+ }
30
+ diskTemperatureSensorChartsTmpl = module.Chart{
31
+ ID: "disk_%s_temperature_sensor_status",
32
+ Title: "Disk temperature sensor status",
33
+ Units: "status",
34
+ Fam: "sensor",
35
+ Ctx: "hddtemp.disk_temperature_sensor_status",
36
+ Type: module.Line,
37
+ Priority: prioDiskTemperatureSensorStatus,
38
+ Dims: module.Dims{
39
+ {ID: "disk_%s_temp_sensor_status_ok", Name: "ok"},
40
+ {ID: "disk_%s_temp_sensor_status_err", Name: "err"},
41
+ {ID: "disk_%s_temp_sensor_status_na", Name: "na"},
42
+ {ID: "disk_%s_temp_sensor_status_unk", Name: "unk"},
43
+ {ID: "disk_%s_temp_sensor_status_nos", Name: "nos"},
44
+ {ID: "disk_%s_temp_sensor_status_slp", Name: "slp"},
45
+ },
46
+ }
47
+)
48
+
49
+func (h *HddTemp) addDiskTempSensorStatusChart(id string, disk diskStats) {
50
+ h.addDiskChart(id, disk, diskTemperatureSensorChartsTmpl.Copy())
51
+}
52
+
53
+func (h *HddTemp) addDiskTempChart(id string, disk diskStats) {
54
+ h.addDiskChart(id, disk, diskTemperatureChartsTmpl.Copy())
55
+}
56
+
57
+func (h *HddTemp) addDiskChart(id string, disk diskStats, chart *module.Chart) {
58
+ chart.ID = fmt.Sprintf(chart.ID, strings.ToLower(id))
59
+ chart.Labels = []module.Label{
60
+ {Key: "disk_id", Value: id},
61
+ {Key: "model", Value: disk.model},
62
+ }
63
+ for _, dim := range chart.Dims {
64
+ dim.ID = fmt.Sprintf(dim.ID, id)
65
+ }
66
+
67
+ if err := h.Charts().Add(chart); err != nil {
68
+ h.Warning(err)
69
+ }
70
+}
src/go/collectors/go.d.plugin/modules/hddtemp/client.go
new
+44
@@ -0,0 +1,44 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package hddtemp
4
+
5
+import (
6
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/socket"
7
+)
8
+
9
+func newHddTempConn(conf Config) hddtempConn {
10
+ return &hddtempClient{conn: socket.New(socket.Config{
11
+ Address: conf.Address,
12
+ ConnectTimeout: conf.Timeout.Duration(),
13
+ ReadTimeout: conf.Timeout.Duration(),
14
+ WriteTimeout: conf.Timeout.Duration(),
15
+ })}
16
+}
17
+
18
+type hddtempClient struct {
19
+ conn socket.Client
20
+}
21
+
22
+func (c *hddtempClient) connect() error {
23
+ return c.conn.Connect()
24
+}
25
+
26
+func (c *hddtempClient) disconnect() {
27
+ _ = c.conn.Disconnect()
28
+}
29
+
30
+func (c *hddtempClient) queryHddTemp() (string, error) {
31
+ var i int
32
+ var s string
33
+ err := c.conn.Command("", func(bytes []byte) bool {
34
+ if i++; i > 1 {
35
+ return false
36
+ }
37
+ s = string(bytes)
38
+ return true
39
+ })
40
+ if err != nil {
41
+ return "", err
42
+ }
43
+ return s, nil
44
+}
src/go/collectors/go.d.plugin/modules/hddtemp/collect.go
new
+140
@@ -0,0 +1,140 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package hddtemp
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "strconv"
9
+ "strings"
10
+)
11
+
12
+type diskStats struct {
13
+ devPath string
14
+ model string
15
+ temperature string
16
+ unit string
17
+}
18
+
19
+func (h *HddTemp) collect() (map[string]int64, error) {
20
+ conn := h.newHddTempConn(h.Config)
21
+
22
+ if err := conn.connect(); err != nil {
23
+ return nil, err
24
+ }
25
+
26
+ defer conn.disconnect()
27
+
28
+ msg, err := conn.queryHddTemp()
29
+ if err != nil {
30
+ return nil, err
31
+ }
32
+
33
+ h.Debugf("hddtemp daemon response: %s", msg)
34
+
35
+ disks, err := parseHddTempMessage(msg)
36
+ if err != nil {
37
+ return nil, err
38
+ }
39
+
40
+ mx := make(map[string]int64)
41
+
42
+ for _, disk := range disks {
43
+ id := getDiskID(disk)
44
+ if id == "" {
45
+ h.Debugf("can not extract disk id from '%s'", disk.devPath)
46
+ continue
47
+ }
48
+
49
+ if !h.disks[id] {
50
+ h.disks[id] = true
51
+ h.addDiskTempSensorStatusChart(id, disk)
52
+ }
53
+
54
+ px := fmt.Sprintf("disk_%s_", id)
55
+
56
+ for _, st := range []string{"ok", "na", "unk", "nos", "slp", "err"} {
57
+ mx[px+"temp_sensor_status_"+st] = 0
58
+ }
59
+ switch disk.temperature {
60
+ case "NA":
61
+ mx[px+"temp_sensor_status_na"] = 1
62
+ case "UNK":
63
+ mx[px+"temp_sensor_status_unk"] = 1
64
+ case "NOS":
65
+ mx[px+"temp_sensor_status_nos"] = 1
66
+ case "SLP":
67
+ mx[px+"temp_sensor_status_slp"] = 1
68
+ case "ERR":
69
+ mx[px+"temp_sensor_status_err"] = 1
70
+ default:
71
+ if v, ok := getTemperature(disk); ok {
72
+ if !h.disksTemp[id] {
73
+ h.disksTemp[id] = true
74
+ h.addDiskTempChart(id, disk)
75
+ }
76
+ mx[px+"temp_sensor_status_ok"] = 1
77
+ mx[px+"temperature"] = v
78
+ } else {
79
+ mx[px+"temp_sensor_status_unk"] = 1
80
+ }
81
+ }
82
+ }
83
+
84
+ return mx, nil
85
+}
86
+
87
+func getDiskID(d diskStats) string {
88
+ i := strings.LastIndexByte(d.devPath, '/')
89
+ if i == -1 {
90
+ return ""
91
+ }
92
+ return d.devPath[i+1:]
93
+}
94
+
95
+func getTemperature(d diskStats) (int64, bool) {
96
+ v, err := strconv.ParseInt(d.temperature, 10, 64)
97
+ if err != nil {
98
+ return 0, false
99
+ }
100
+ if d.unit == "F" {
101
+ v = (v - 32) * 5 / 9
102
+ }
103
+ return v, true
104
+}
105
+
106
+func parseHddTempMessage(msg string) ([]diskStats, error) {
107
+ if msg == "" {
108
+ return nil, errors.New("empty hddtemp message")
109
+ }
110
+
111
+ // https://github.com/guzu/hddtemp/blob/e16aed6d0145d7ad8b3308dd0b9199fc701c0417/src/daemon.c#L165
112
+ parts := strings.Split(msg, "|")
113
+
114
+ var i int
115
+ // remove empty values
116
+ for _, v := range parts {
117
+ if v = strings.TrimSpace(v); v != "" {
118
+ parts[i] = v
119
+ i++
120
+ }
121
+ }
122
+ parts = parts[:i]
123
+
124
+ if len(parts) == 0 || len(parts)%4 != 0 {
125
+ return nil, errors.New("invalid hddtemp output format")
126
+ }
127
+
128
+ var disks []diskStats
129
+
130
+ for i := 0; i < len(parts); i += 4 {
131
+ disks = append(disks, diskStats{
132
+ devPath: parts[i],
133
+ model: parts[i+1],
134
+ temperature: parts[i+2],
135
+ unit: parts[i+3],
136
+ })
137
+ }
138
+
139
+ return disks, nil
140
+}
src/go/collectors/go.d.plugin/modules/hddtemp/config_schema.json
new
+44
@@ -0,0 +1,44 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "HddTemp 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
+ "address": {
15
+ "title": "Address",
16
+ "description": "The IP address and port where the hddtemp daemon listens for connections.",
17
+ "type": "string",
18
+ "default": "127.0.0.1:7634"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 1
26
+ }
27
+ },
28
+ "required": [
29
+ "address"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "timeout": {
41
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
42
+ }
43
+ }
44
+}
src/go/collectors/go.d.plugin/modules/hddtemp/hddtemp.go
new
+104
@@ -0,0 +1,104 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package hddtemp
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("hddtemp", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Create: func() module.Module { return New() },
21
+ })
22
+}
23
+
24
+func New() *HddTemp {
25
+ return &HddTemp{
26
+ Config: Config{
27
+ Address: "127.0.0.1:7634",
28
+ Timeout: web.Duration(time.Second * 1),
29
+ },
30
+ newHddTempConn: newHddTempConn,
31
+ charts: &module.Charts{},
32
+ disks: make(map[string]bool),
33
+ disksTemp: make(map[string]bool),
34
+ }
35
+}
36
+
37
+type Config struct {
38
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
39
+ Address string `yaml:"address" json:"address"`
40
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
41
+}
42
+
43
+type (
44
+ HddTemp struct {
45
+ module.Base
46
+ Config `yaml:",inline" json:""`
47
+
48
+ charts *module.Charts
49
+
50
+ newHddTempConn func(Config) hddtempConn
51
+
52
+ disks map[string]bool
53
+ disksTemp map[string]bool
54
+ }
55
+
56
+ hddtempConn interface {
57
+ connect() error
58
+ disconnect()
59
+ queryHddTemp() (string, error)
60
+ }
61
+)
62
+
63
+func (h *HddTemp) Configuration() any {
64
+ return h.Config
65
+}
66
+
67
+func (h *HddTemp) Init() error {
68
+ if h.Address == "" {
69
+ h.Error("config: 'address' not set")
70
+ return errors.New("address not set")
71
+ }
72
+
73
+ return nil
74
+}
75
+
76
+func (h *HddTemp) Check() error {
77
+ mx, err := h.collect()
78
+ if err != nil {
79
+ h.Error(err)
80
+ return err
81
+ }
82
+ if len(mx) == 0 {
83
+ return errors.New("no metrics collected")
84
+ }
85
+ return nil
86
+}
87
+
88
+func (h *HddTemp) Charts() *module.Charts {
89
+ return h.charts
90
+}
91
+
92
+func (h *HddTemp) Collect() map[string]int64 {
93
+ mx, err := h.collect()
94
+ if err != nil {
95
+ h.Error(err)
96
+ }
97
+
98
+ if len(mx) == 0 {
99
+ return nil
100
+ }
101
+ return mx
102
+}
103
+
104
+func (h *HddTemp) Cleanup() {}
src/go/collectors/go.d.plugin/modules/hddtemp/hddtemp_test.go
new
+321
@@ -0,0 +1,321 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package hddtemp
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
+ dataAllOK, _ = os.ReadFile("testdata/hddtemp-all-ok.txt")
21
+ dataAllSleep, _ = os.ReadFile("testdata/hddtemp-all-sleep.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
+ "dataAllOK": dataAllOK,
30
+ "dataAllSleep": dataAllSleep,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+ }
34
+}
35
+
36
+func TestHddTemp_ConfigurationSerialize(t *testing.T) {
37
+ module.TestConfigurationSerialize(t, &HddTemp{}, dataConfigJSON, dataConfigYAML)
38
+}
39
+
40
+func TestHddTemp_Init(t *testing.T) {
41
+ tests := map[string]struct {
42
+ config Config
43
+ wantFail bool
44
+ }{
45
+ "success with default config": {
46
+ wantFail: false,
47
+ config: New().Config,
48
+ },
49
+ "fails if address not set": {
50
+ wantFail: true,
51
+ config: func() Config {
52
+ conf := New().Config
53
+ conf.Address = ""
54
+ return conf
55
+ }(),
56
+ },
57
+ }
58
+
59
+ for name, test := range tests {
60
+ t.Run(name, func(t *testing.T) {
61
+ hdd := New()
62
+ hdd.Config = test.config
63
+
64
+ if test.wantFail {
65
+ assert.Error(t, hdd.Init())
66
+ } else {
67
+ assert.NoError(t, hdd.Init())
68
+ }
69
+ })
70
+ }
71
+}
72
+
73
+func TestHddTemp_Cleanup(t *testing.T) {
74
+ tests := map[string]struct {
75
+ prepare func() *HddTemp
76
+ }{
77
+ "not initialized": {
78
+ prepare: func() *HddTemp {
79
+ return New()
80
+ },
81
+ },
82
+ "after check": {
83
+ prepare: func() *HddTemp {
84
+ hdd := New()
85
+ hdd.newHddTempConn = func(config Config) hddtempConn { return prepareMockAllDisksOk() }
86
+ _ = hdd.Check()
87
+ return hdd
88
+ },
89
+ },
90
+ "after collect": {
91
+ prepare: func() *HddTemp {
92
+ hdd := New()
93
+ hdd.newHddTempConn = func(config Config) hddtempConn { return prepareMockAllDisksOk() }
94
+ _ = hdd.Collect()
95
+ return hdd
96
+ },
97
+ },
98
+ }
99
+
100
+ for name, test := range tests {
101
+ t.Run(name, func(t *testing.T) {
102
+ hdd := test.prepare()
103
+
104
+ assert.NotPanics(t, hdd.Cleanup)
105
+ })
106
+ }
107
+}
108
+
109
+func TestHddTemp_Charts(t *testing.T) {
110
+ assert.NotNil(t, New().Charts())
111
+}
112
+
113
+func TestHddTemp_Check(t *testing.T) {
114
+ tests := map[string]struct {
115
+ prepareMock func() *mockHddTempConn
116
+ wantFail bool
117
+ }{
118
+ "all disks ok": {
119
+ wantFail: false,
120
+ prepareMock: prepareMockAllDisksOk,
121
+ },
122
+ "all disks sleep": {
123
+ wantFail: false,
124
+ prepareMock: prepareMockAllDisksSleep,
125
+ },
126
+ "err on connect": {
127
+ wantFail: true,
128
+ prepareMock: prepareMockErrOnConnect,
129
+ },
130
+ "unexpected response": {
131
+ wantFail: true,
132
+ prepareMock: prepareMockUnexpectedResponse,
133
+ },
134
+ "empty response": {
135
+ wantFail: true,
136
+ prepareMock: prepareMockEmptyResponse,
137
+ },
138
+ }
139
+
140
+ for name, test := range tests {
141
+ t.Run(name, func(t *testing.T) {
142
+ hdd := New()
143
+ mock := test.prepareMock()
144
+ hdd.newHddTempConn = func(config Config) hddtempConn { return mock }
145
+
146
+ if test.wantFail {
147
+ assert.Error(t, hdd.Check())
148
+ } else {
149
+ assert.NoError(t, hdd.Check())
150
+ }
151
+ })
152
+ }
153
+}
154
+
155
+func TestHddTemp_Collect(t *testing.T) {
156
+ tests := map[string]struct {
157
+ prepareMock func() *mockHddTempConn
158
+ wantMetrics map[string]int64
159
+ wantDisconnect bool
160
+ wantCharts int
161
+ }{
162
+ "all disks ok": {
163
+ prepareMock: prepareMockAllDisksOk,
164
+ wantDisconnect: true,
165
+ wantCharts: 2 * 4,
166
+ wantMetrics: map[string]int64{
167
+ "disk_sda_temp_sensor_status_err": 0,
168
+ "disk_sda_temp_sensor_status_na": 0,
169
+ "disk_sda_temp_sensor_status_nos": 0,
170
+ "disk_sda_temp_sensor_status_ok": 1,
171
+ "disk_sda_temp_sensor_status_slp": 0,
172
+ "disk_sda_temp_sensor_status_unk": 0,
173
+ "disk_sda_temperature": 50,
174
+ "disk_sdb_temp_sensor_status_err": 0,
175
+ "disk_sdb_temp_sensor_status_na": 0,
176
+ "disk_sdb_temp_sensor_status_nos": 0,
177
+ "disk_sdb_temp_sensor_status_ok": 1,
178
+ "disk_sdb_temp_sensor_status_slp": 0,
179
+ "disk_sdb_temp_sensor_status_unk": 0,
180
+ "disk_sdb_temperature": 49,
181
+ "disk_sdc_temp_sensor_status_err": 0,
182
+ "disk_sdc_temp_sensor_status_na": 0,
183
+ "disk_sdc_temp_sensor_status_nos": 0,
184
+ "disk_sdc_temp_sensor_status_ok": 1,
185
+ "disk_sdc_temp_sensor_status_slp": 0,
186
+ "disk_sdc_temp_sensor_status_unk": 0,
187
+ "disk_sdc_temperature": 27,
188
+ "disk_sdd_temp_sensor_status_err": 0,
189
+ "disk_sdd_temp_sensor_status_na": 0,
190
+ "disk_sdd_temp_sensor_status_nos": 0,
191
+ "disk_sdd_temp_sensor_status_ok": 1,
192
+ "disk_sdd_temp_sensor_status_slp": 0,
193
+ "disk_sdd_temp_sensor_status_unk": 0,
194
+ "disk_sdd_temperature": 29,
195
+ },
196
+ },
197
+ "all disks sleep": {
198
+ prepareMock: prepareMockAllDisksSleep,
199
+ wantDisconnect: true,
200
+ wantCharts: 3,
201
+ wantMetrics: map[string]int64{
202
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_err": 0,
203
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_na": 0,
204
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_nos": 0,
205
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_ok": 0,
206
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_slp": 1,
207
+ "disk_ata-HUP722020APA330_BFGWU7WF_temp_sensor_status_unk": 0,
208
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_err": 0,
209
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_na": 0,
210
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_nos": 0,
211
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_ok": 0,
212
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_slp": 1,
213
+ "disk_ata-HUP722020APA330_BFJ0WS3F_temp_sensor_status_unk": 0,
214
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_err": 0,
215
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_na": 0,
216
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_nos": 0,
217
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_ok": 0,
218
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_slp": 1,
219
+ "disk_ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922_temp_sensor_status_unk": 0,
220
+ },
221
+ },
222
+ "err on connect": {
223
+ prepareMock: prepareMockErrOnConnect,
224
+ wantDisconnect: false,
225
+ },
226
+ "unexpected response": {
227
+ prepareMock: prepareMockUnexpectedResponse,
228
+ wantDisconnect: true,
229
+ },
230
+ "empty response": {
231
+ prepareMock: prepareMockEmptyResponse,
232
+ wantDisconnect: true,
233
+ },
234
+ }
235
+
236
+ for name, test := range tests {
237
+ t.Run(name, func(t *testing.T) {
238
+ hdd := New()
239
+ mock := test.prepareMock()
240
+ hdd.newHddTempConn = func(config Config) hddtempConn { return mock }
241
+
242
+ mx := hdd.Collect()
243
+
244
+ assert.Equal(t, test.wantMetrics, mx)
245
+ assert.Len(t, *hdd.Charts(), test.wantCharts)
246
+ assert.Equal(t, test.wantDisconnect, mock.disconnectCalled)
247
+ testMetricsHasAllChartsDims(t, hdd, mx)
248
+ })
249
+ }
250
+}
251
+
252
+func testMetricsHasAllChartsDims(t *testing.T, hdd *HddTemp, mx map[string]int64) {
253
+ for _, chart := range *hdd.Charts() {
254
+ if chart.Obsolete {
255
+ continue
256
+ }
257
+ for _, dim := range chart.Dims {
258
+ _, ok := mx[dim.ID]
259
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
260
+ }
261
+ for _, v := range chart.Vars {
262
+ _, ok := mx[v.ID]
263
+ assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
264
+ }
265
+ }
266
+}
267
+
268
+func prepareMockAllDisksOk() *mockHddTempConn {
269
+ return &mockHddTempConn{
270
+ hddTempLine: string(dataAllOK),
271
+ }
272
+}
273
+
274
+func prepareMockAllDisksSleep() *mockHddTempConn {
275
+ return &mockHddTempConn{
276
+ hddTempLine: string(dataAllSleep),
277
+ }
278
+}
279
+
280
+func prepareMockErrOnConnect() *mockHddTempConn {
281
+ return &mockHddTempConn{
282
+ errOnConnect: true,
283
+ }
284
+}
285
+
286
+func prepareMockUnexpectedResponse() *mockHddTempConn {
287
+ return &mockHddTempConn{
288
+ hddTempLine: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
289
+ }
290
+}
291
+
292
+func prepareMockEmptyResponse() *mockHddTempConn {
293
+ return &mockHddTempConn{
294
+ hddTempLine: "",
295
+ }
296
+}
297
+
298
+type mockHddTempConn struct {
299
+ errOnConnect bool
300
+ errOnQueryHddTemp bool
301
+ hddTempLine string
302
+ disconnectCalled bool
303
+}
304
+
305
+func (m *mockHddTempConn) connect() error {
306
+ if m.errOnConnect {
307
+ return errors.New("mock.connect() error")
308
+ }
309
+ return nil
310
+}
311
+
312
+func (m *mockHddTempConn) disconnect() {
313
+ m.disconnectCalled = true
314
+}
315
+
316
+func (m *mockHddTempConn) queryHddTemp() (string, error) {
317
+ if m.errOnQueryHddTemp {
318
+ return "", errors.New("mock.queryHddTemp() error")
319
+ }
320
+ return m.hddTempLine, nil
321
+}
src/go/collectors/go.d.plugin/modules/hddtemp/metadata.yaml
new
+134
@@ -0,0 +1,134 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-hddtemp
5
+ plugin_name: go.d.plugin
6
+ module_name: hddtemp
7
+ monitored_instance:
8
+ name: HDD temperature
9
+ link: https://linux.die.net/man/8/hddtemp
10
+ categories:
11
+ - data-collection.hardware-devices-and-sensors
12
+ icon_filename: "hard-drive.svg"
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ info_provided_to_referring_integrations:
17
+ description: ""
18
+ keywords:
19
+ - hardware
20
+ - hdd temperature
21
+ - disk temperature
22
+ - temperature
23
+ most_popular: false
24
+ overview:
25
+ data_collection:
26
+ metrics_description: |
27
+ This collector monitors disk temperatures.
28
+ method_description: |
29
+ It retrieves temperature data for attached disks by querying the hddtemp daemon at regular intervals.
30
+ supported_platforms:
31
+ include:
32
+ - Linux
33
+ exclude: []
34
+ multi_instance: true
35
+ additional_permissions:
36
+ description: ""
37
+ default_behavior:
38
+ auto_detection:
39
+ description: By default, this collector will attempt to connect to the `hddtemp` daemon on `127.0.0.1:7634`
40
+ limits:
41
+ description: ""
42
+ performance_impact:
43
+ description: ""
44
+ setup:
45
+ prerequisites:
46
+ list:
47
+ - title: Install hddtemp
48
+ description: |
49
+ Install `hddtemp` using your distribution's package manager.
50
+ configuration:
51
+ file:
52
+ name: go.d/hddtemp.conf
53
+ options:
54
+ description: |
55
+ The following options can be defined globally: update_every, autodetection_retry.
56
+ folding:
57
+ title: Config options
58
+ enabled: true
59
+ list:
60
+ - name: update_every
61
+ description: Data collection frequency.
62
+ default_value: 1
63
+ required: false
64
+ - name: autodetection_retry
65
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
66
+ default_value: 0
67
+ required: false
68
+ - name: address
69
+ description: The IP address and port where the hddtemp daemon listens for connections.
70
+ default_value: 127.0.0.1:7634
71
+ required: true
72
+ - name: timeout
73
+ description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
74
+ default_value: 1
75
+ required: false
76
+ examples:
77
+ folding:
78
+ title: Config
79
+ enabled: true
80
+ list:
81
+ - name: Basic
82
+ description: A basic example configuration.
83
+ config: |
84
+ jobs:
85
+ - name: local
86
+ address: 127.0.0.1:7634
87
+ - name: Multi-instance
88
+ description: |
89
+ > **Note**: When you define multiple jobs, their names must be unique.
90
+
91
+ Collecting metrics from local and remote instances.
92
+ config: |
93
+ jobs:
94
+ - name: local
95
+ address: 127.0.0.1:7634
96
+
97
+ - name: remote
98
+ address: 203.0.113.0:7634
99
+ troubleshooting:
100
+ problems:
101
+ list: []
102
+ alerts: []
103
+ metrics:
104
+ folding:
105
+ title: Metrics
106
+ enabled: false
107
+ description: ""
108
+ availability: []
109
+ scopes:
110
+ - name: disk
111
+ description: These metrics refer to the Disk.
112
+ labels:
113
+ - name: disk_id
114
+ description: Disk identifier. It is derived from the device path (e.g. sda or ata-HUP722020APA330_BFJ0WS3F)
115
+ - name: model
116
+ description: Disk model
117
+ metrics:
118
+ - name: hddtemp.disk_temperature
119
+ description: Disk temperature
120
+ unit: Celsius
121
+ chart_type: line
122
+ dimensions:
123
+ - name: temperature
124
+ - name: hddtemp.disk_temperature_sensor_status
125
+ description: Disk temperature sensor status
126
+ unit: status
127
+ chart_type: line
128
+ dimensions:
129
+ - name: ok
130
+ - name: err
131
+ - name: na
132
+ - name: unk
133
+ - name: nos
134
+ - name: slp
src/go/collectors/go.d.plugin/modules/hddtemp/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "address": "ok",
4
+ "timeout": 123.123
5
+}
src/go/collectors/go.d.plugin/modules/hddtemp/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+address: "ok"
3
+timeout: 123.123
src/go/collectors/go.d.plugin/modules/hddtemp/testdata/hddtemp-all-ok.txt
new
+1
@@ -0,0 +1 @@
1
+|/dev/sda|WDC WD181KRYZ-01AGBB0|122|F||/dev/sdb|WDC WD181KRYZ-01AGBB0|49|C||/dev/sdc|WDC WDS400T1R0A-68A4W0|27|C||/dev/sdd|WDC WDS400T1R0A-68A4W0|29|C|
\ No newline at end of file
src/go/collectors/go.d.plugin/modules/hddtemp/testdata/hddtemp-all-sleep.txt
new
+1
@@ -0,0 +1 @@
1
+|/dev/disk/by-id/ata-HUP722020APA330_BFJ0WS3F|HUP722020APA330|SLP|*||/dev/disk/by-id/ata-HUP722020APA330_BFGWU7WF|HUP722020APA330|SLP|*||/dev/disk/by-id/ata-WDC_WD10EARS-00Y5B1_WD-WCAV5R693922|WDC WD10EARS-00Y5B1|SLP|*|
\ No newline at end of file
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -29,6 +29,7 @@ import (
29
_ "github.com/netdata/netdata/go/go.d.plugin/modules/freeradius"
30
_ "github.com/netdata/netdata/go/go.d.plugin/modules/geth"
31
_ "github.com/netdata/netdata/go/go.d.plugin/modules/haproxy"
32
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/hddtemp"
33
_ "github.com/netdata/netdata/go/go.d.plugin/modules/hdfs"
34
_ "github.com/netdata/netdata/go/go.d.plugin/modules/httpcheck"
35
_ "github.com/netdata/netdata/go/go.d.plugin/modules/intelgpu"