@cryptotaxi247 / netdata-1 / commits / 7826adcf6

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"