add collector to monitor ZFS pools space usage (#17367)
Ilya Mashchenko committed
Apr 10, 2024 at 20:18 UTC
7b4b258879ba4a83817981033dd6342a70cf80af
18 files changed
+985
-1
src/collectors/proc.plugin/plugin_proc.c
+1
@@ -160,6 +160,7 @@ void *proc_main(void *ptr)
160
netdata_thread_cleanup_push(proc_main_cleanup, ptr)
161
{
162
config_get_boolean("plugin:proc", "/proc/pagetypeinfo", CONFIG_BOOLEAN_NO);
163
+ config_get_boolean("plugin:proc", "/proc/spl/kstat/zfs/pool/state", CONFIG_BOOLEAN_NO);
164
165
// check the enabled status for each module
166
int i;
src/go/collectors/go.d.plugin/README.md
+1
@@ -128,6 +128,7 @@ see the appropriate collector readme.
128
| [whoisquery](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/whoisquery) | Domain Expiry |
129
| [windows](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/windows) | Windows |
130
| [x509check](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/x509check) | Digital Certificates |
131
+| [zfspool](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/zfspool) | ZFS Pools |
132
| [zookeeper](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/zookeeper) | ZooKeeper |
133
134
## Configuration
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -88,4 +88,5 @@ modules:
88
# whoisquery: yes
89
# windows: yes
90
# x509check: yes
91
+# zfspool: yes
92
# zookeeper: yes
src/go/collectors/go.d.plugin/config/go.d/zfspool.conf
new
+9
@@ -0,0 +1,9 @@
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/zpool#readme
3
+
4
+jobs:
5
+ - name: zfspool
6
+ binary_path: /usr/bin/zpool
7
+
8
+ - name: zfspool
9
+ binary_path: /sbin/zpool # FreeBSD
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -80,5 +80,6 @@ import (
80
_ "github.com/netdata/netdata/go/go.d.plugin/modules/windows"
81
_ "github.com/netdata/netdata/go/go.d.plugin/modules/wireguard"
82
_ "github.com/netdata/netdata/go/go.d.plugin/modules/x509check"
83
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/zfspool"
84
_ "github.com/netdata/netdata/go/go.d.plugin/modules/zookeeper"
85
)
src/go/collectors/go.d.plugin/modules/nvme/config_schema.json
+1
-1
@@ -1,7 +1,7 @@
1
{
2
"jsonSchema": {
3
"$schema": "http://json-schema.org/draft-07/schema#",
4
- "title": "NVMe Collector Configuration",
4
+ "title": "NVMe collector configuration",
5
"type": "object",
6
"properties": {
7
"update_every": {
src/go/collectors/go.d.plugin/modules/zfspool/charts.go
new
+115
@@ -0,0 +1,115 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
10
+)
11
+
12
+const (
13
+ prioZpoolSpaceUtilization = 2820 + iota
14
+ prioZpoolSpaceUsage
15
+ prioZpoolFragmentation
16
+ prioZpoolHealthState
17
+)
18
+
19
+var zpoolChartsTmpl = module.Charts{
20
+ zpoolSpaceUtilizationChartTmpl.Copy(),
21
+ zpoolSpaceUsageChartTmpl.Copy(),
22
+
23
+ zpoolFragmentationChartTmpl.Copy(),
24
+
25
+ zpoolHealthStateChartTmpl.Copy(),
26
+}
27
+
28
+var (
29
+ zpoolSpaceUtilizationChartTmpl = module.Chart{
30
+ ID: "zfspool_%s_space_utilization",
31
+ Title: "Zpool space utilization",
32
+ Units: "percentage",
33
+ Fam: "space usage",
34
+ Ctx: "zfspool.pool_space_utilization",
35
+ Type: module.Area,
36
+ Priority: prioZpoolSpaceUtilization,
37
+ Dims: module.Dims{
38
+ {ID: "zpool_%s_cap", Name: "utilization"},
39
+ },
40
+ }
41
+ zpoolSpaceUsageChartTmpl = module.Chart{
42
+ ID: "zfspool_%s_space_usage",
43
+ Title: "Zpool space usage",
44
+ Units: "bytes",
45
+ Fam: "space usage",
46
+ Ctx: "zfspool.pool_space_usage",
47
+ Type: module.Stacked,
48
+ Priority: prioZpoolSpaceUsage,
49
+ Dims: module.Dims{
50
+ {ID: "zpool_%s_free", Name: "free"},
51
+ {ID: "zpool_%s_alloc", Name: "used"},
52
+ },
53
+ }
54
+
55
+ zpoolFragmentationChartTmpl = module.Chart{
56
+ ID: "zfspool_%s_fragmentation",
57
+ Title: "Zpool fragmentation",
58
+ Units: "percentage",
59
+ Fam: "fragmentation",
60
+ Ctx: "zfspool.pool_fragmentation",
61
+ Type: module.Line,
62
+ Priority: prioZpoolFragmentation,
63
+ Dims: module.Dims{
64
+ {ID: "zpool_%s_frag", Name: "fragmentation"},
65
+ },
66
+ }
67
+
68
+ zpoolHealthStateChartTmpl = module.Chart{
69
+ ID: "zfspool_%s_health_state",
70
+ Title: "Zpool health state",
71
+ Units: "state",
72
+ Fam: "health",
73
+ Ctx: "zfspool.pool_health_state",
74
+ Type: module.Line,
75
+ Priority: prioZpoolHealthState,
76
+ Dims: module.Dims{
77
+ {ID: "zpool_%s_health_state_online", Name: "online"},
78
+ {ID: "zpool_%s_health_state_degraded", Name: "degraded"},
79
+ {ID: "zpool_%s_health_state_faulted", Name: "faulted"},
80
+ {ID: "zpool_%s_health_state_offline", Name: "offline"},
81
+ {ID: "zpool_%s_health_state_unavail", Name: "unavail"},
82
+ {ID: "zpool_%s_health_state_removed", Name: "removed"},
83
+ {ID: "zpool_%s_health_state_suspended", Name: "suspended"},
84
+ },
85
+ }
86
+)
87
+
88
+func (z *ZFSPool) addZpoolCharts(name string) {
89
+ charts := zpoolChartsTmpl.Copy()
90
+
91
+ for _, chart := range *charts {
92
+ chart.ID = fmt.Sprintf(chart.ID, name)
93
+ chart.Labels = []module.Label{
94
+ {Key: "pool", Value: name},
95
+ }
96
+ for _, dim := range chart.Dims {
97
+ dim.ID = fmt.Sprintf(dim.ID, name)
98
+ }
99
+ }
100
+
101
+ if err := z.Charts().Add(*charts...); err != nil {
102
+ z.Warning(err)
103
+ }
104
+}
105
+
106
+func (z *ZFSPool) removeZpoolCharts(name string) {
107
+ px := fmt.Sprintf("zpool_%s_", name)
108
+
109
+ for _, chart := range *z.Charts() {
110
+ if strings.HasPrefix(chart.ID, px) {
111
+ chart.MarkRemove()
112
+ chart.MarkNotCreated()
113
+ }
114
+ }
115
+}
src/go/collectors/go.d.plugin/modules/zfspool/collect.go
new
+177
@@ -0,0 +1,177 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "fmt"
9
+ "strconv"
10
+ "strings"
11
+)
12
+
13
+var zpoolHealthStates = []string{
14
+ "online",
15
+ "degraded",
16
+ "faulted",
17
+ "offline",
18
+ "removed",
19
+ "unavail",
20
+ "suspended",
21
+}
22
+
23
+type zpoolStats struct {
24
+ name string
25
+ sizeBytes string
26
+ allocBytes string
27
+ freeBytes string
28
+ fragPerc string
29
+ capPerc string
30
+ dedupRatio string
31
+ health string
32
+}
33
+
34
+func (z *ZFSPool) collect() (map[string]int64, error) {
35
+ bs, err := z.exec.list()
36
+ if err != nil {
37
+ return nil, err
38
+ }
39
+
40
+ zpools, err := parseZpoolListOutput(bs)
41
+ if err != nil {
42
+ return nil, err
43
+ }
44
+
45
+ mx := make(map[string]int64)
46
+
47
+ z.collectZpoolListStats(mx, zpools)
48
+
49
+ return mx, nil
50
+}
51
+
52
+func (z *ZFSPool) collectZpoolListStats(mx map[string]int64, zpools []zpoolStats) {
53
+ seen := make(map[string]bool)
54
+
55
+ for _, zpool := range zpools {
56
+ seen[zpool.name] = true
57
+
58
+ if !z.zpools[zpool.name] {
59
+ z.addZpoolCharts(zpool.name)
60
+ z.zpools[zpool.name] = true
61
+ }
62
+
63
+ px := "zpool_" + zpool.name + "_"
64
+
65
+ if v, ok := parseInt(zpool.sizeBytes); ok {
66
+ mx[px+"size"] = v
67
+ }
68
+ if v, ok := parseInt(zpool.freeBytes); ok {
69
+ mx[px+"free"] = v
70
+ }
71
+ if v, ok := parseInt(zpool.allocBytes); ok {
72
+ mx[px+"alloc"] = v
73
+ }
74
+ if v, ok := parseFloat(zpool.capPerc); ok {
75
+ mx[px+"cap"] = int64(v)
76
+ }
77
+ if v, ok := parseFloat(zpool.fragPerc); ok {
78
+ mx[px+"frag"] = int64(v)
79
+ }
80
+ for _, s := range zpoolHealthStates {
81
+ mx[px+"health_state_"+s] = 0
82
+ }
83
+ mx[px+"health_state_"+zpool.health] = 1
84
+ }
85
+
86
+ for name := range z.zpools {
87
+ if !seen[name] {
88
+ z.removeZpoolCharts(name)
89
+ delete(z.zpools, name)
90
+ }
91
+ }
92
+}
93
+
94
+func parseZpoolListOutput(bs []byte) ([]zpoolStats, error) {
95
+ var lines []string
96
+ sc := bufio.NewScanner(bytes.NewReader(bs))
97
+ for sc.Scan() {
98
+ if text := strings.TrimSpace(sc.Text()); text != "" {
99
+ lines = append(lines, text)
100
+ }
101
+
102
+ }
103
+ if len(lines) < 2 {
104
+ return nil, fmt.Errorf("unexpected data: wanted >= 2 lines, got %d", len(lines))
105
+ }
106
+
107
+ headers := strings.Fields(lines[0])
108
+ if len(headers) == 0 {
109
+ return nil, fmt.Errorf("unexpected data: missing headers")
110
+ }
111
+
112
+ var zpools []zpoolStats
113
+
114
+ /*
115
+ # zpool list -p
116
+ NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
117
+ rpool 21367462298 9051643576 12240656794 - 33 42 1.00 ONLINE -
118
+ zion - - - - - - - FAULTED -
119
+ */
120
+
121
+ for _, line := range lines[1:] {
122
+ values := strings.Fields(line)
123
+ if len(values) != len(headers) {
124
+ return nil, fmt.Errorf("unequal columns: headers(%d) != values(%d)", len(headers), len(values))
125
+ }
126
+
127
+ var zpool zpoolStats
128
+
129
+ for i, v := range values {
130
+ v = strings.TrimSpace(v)
131
+ switch strings.ToLower(headers[i]) {
132
+ case "name":
133
+ zpool.name = v
134
+ case "size":
135
+ zpool.sizeBytes = v
136
+ case "alloc":
137
+ zpool.allocBytes = v
138
+ case "free":
139
+ zpool.freeBytes = v
140
+ case "frag":
141
+ zpool.fragPerc = v
142
+ case "cap":
143
+ zpool.capPerc = v
144
+ case "dedup":
145
+ zpool.dedupRatio = v
146
+ case "health":
147
+ zpool.health = strings.ToLower(v)
148
+ }
149
+
150
+ if last := i+1 == len(headers); last && zpool.name != "" && zpool.health != "" {
151
+ zpools = append(zpools, zpool)
152
+ }
153
+ }
154
+ }
155
+
156
+ if len(zpools) == 0 {
157
+ return nil, fmt.Errorf("unexpected data: missing pools")
158
+ }
159
+
160
+ return zpools, nil
161
+}
162
+
163
+func parseInt(s string) (int64, bool) {
164
+ if s == "-" {
165
+ return 0, false
166
+ }
167
+ v, err := strconv.ParseInt(s, 10, 64)
168
+ return v, err == nil
169
+}
170
+
171
+func parseFloat(s string) (float64, bool) {
172
+ if s == "-" {
173
+ return 0, false
174
+ }
175
+ v, err := strconv.ParseFloat(s, 64)
176
+ return v, err == nil
177
+}
src/go/collectors/go.d.plugin/modules/zfspool/config_schema.json
new
+47
@@ -0,0 +1,47 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "ZFS Pools 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": 10
13
+ },
14
+ "binary_path": {
15
+ "title": "Binary path",
16
+ "description": "Path to the `zpool` binary.",
17
+ "type": "string",
18
+ "default": "nvme"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for executing the binary, specified in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 10
26
+ }
27
+ },
28
+ "required": [
29
+ "binary_path"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "binary_path": {
41
+ "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."
42
+ },
43
+ "timeout": {
44
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
45
+ }
46
+ }
47
+}
src/go/collectors/go.d.plugin/modules/zfspool/exec.go
new
+41
@@ -0,0 +1,41 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
4
+
5
+import (
6
+ "context"
7
+ "fmt"
8
+ "os/exec"
9
+ "time"
10
+
11
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
12
+)
13
+
14
+func newZpoolCLIExec(binPath string, timeout time.Duration) *zpoolCLIExec {
15
+ return &zpoolCLIExec{
16
+ binPath: binPath,
17
+ timeout: timeout,
18
+ }
19
+}
20
+
21
+type zpoolCLIExec struct {
22
+ *logger.Logger
23
+
24
+ binPath string
25
+ timeout time.Duration
26
+}
27
+
28
+func (e *zpoolCLIExec) list() ([]byte, error) {
29
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
30
+ defer cancel()
31
+
32
+ cmd := exec.CommandContext(ctx, e.binPath, "list", "-p")
33
+ e.Debugf("executing '%s'", cmd)
34
+
35
+ bs, err := cmd.Output()
36
+ if err != nil {
37
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
38
+ }
39
+
40
+ return bs, nil
41
+}
src/go/collectors/go.d.plugin/modules/zfspool/init.go
new
+37
@@ -0,0 +1,37 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+)
11
+
12
+func (z *ZFSPool) validateConfig() error {
13
+ if z.BinaryPath == "" {
14
+ return errors.New("no zpool binary path specified")
15
+ }
16
+ return nil
17
+}
18
+
19
+func (z *ZFSPool) initZPoolCLIExec() (zpoolCLI, error) {
20
+ binPath := z.BinaryPath
21
+
22
+ if !strings.HasPrefix(binPath, "/") {
23
+ path, err := exec.LookPath(binPath)
24
+ if err != nil {
25
+ return nil, err
26
+ }
27
+ binPath = path
28
+ }
29
+
30
+ if _, err := os.Stat(binPath); err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ zpoolExec := newZpoolCLIExec(binPath, z.Timeout.Duration())
35
+
36
+ return zpoolExec, nil
37
+}
src/go/collectors/go.d.plugin/modules/zfspool/metadata.yaml
new
+138
@@ -0,0 +1,138 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-zfspool
5
+ plugin_name: go.d.plugin
6
+ module_name: zfspool
7
+ monitored_instance:
8
+ name: ZFS Pools
9
+ link: ""
10
+ icon_filename: filesystem.svg
11
+ categories:
12
+ - data-collection.storage-mount-points-and-filesystems
13
+ keywords:
14
+ - zfs pools
15
+ - pools
16
+ - zfs
17
+ - filesystem
18
+ related_resources:
19
+ integrations:
20
+ list: []
21
+ info_provided_to_referring_integrations:
22
+ description: ""
23
+ most_popular: false
24
+ overview:
25
+ data_collection:
26
+ metrics_description: >
27
+ This collector monitors the health and space usage of ZFS pools using the command line
28
+ tool [zpool](https://openzfs.github.io/openzfs-docs/man/master/8/zpool-list.8.html).
29
+ method_description: ""
30
+ supported_platforms:
31
+ include: []
32
+ exclude: []
33
+ multi_instance: false
34
+ additional_permissions:
35
+ description: ""
36
+ default_behavior:
37
+ auto_detection:
38
+ description: ""
39
+ limits:
40
+ description: ""
41
+ performance_impact:
42
+ description: ""
43
+ setup:
44
+ prerequisites:
45
+ list: []
46
+ configuration:
47
+ file:
48
+ name: go.d/zfspool.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: 10
59
+ required: false
60
+ - name: binary_path
61
+ description: Path to the `zpool` 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.
62
+ default_value: /usr/bin/zpool
63
+ required: true
64
+ - name: timeout
65
+ description: Timeout for executing the binary, specified in seconds.
66
+ default_value: 2
67
+ required: false
68
+ examples:
69
+ folding:
70
+ title: Config
71
+ enabled: true
72
+ list:
73
+ - name: Custom binary path
74
+ description: The executable is not in the directories specified in the PATH environment variable.
75
+ config: |
76
+ jobs:
77
+ - name: zfspool
78
+ binary_path: /usr/local/sbin/zpool
79
+ troubleshooting:
80
+ problems:
81
+ list: []
82
+ alerts:
83
+ - name: zfs_pool_space_utilization
84
+ metric: zfspool.pool_space_utilization
85
+ info: "ZFS pool **${label:pool}** is nearing capacity. Current space usage is above the threshold."
86
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
87
+ - name: zfs_pool_health_state_warn
88
+ metric: zfspool.pool_health_state
89
+ info: "ZFS pool ${label:pool} state is degraded"
90
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
91
+ - name: zfs_pool_health_state_crit
92
+ metric: zfspool.pool_health_state
93
+ info: "ZFS pool ${label:pool} state is faulted or unavail"
94
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
95
+ metrics:
96
+ folding:
97
+ title: Metrics
98
+ enabled: false
99
+ description: ""
100
+ availability: []
101
+ scopes:
102
+ - name: zfs pool
103
+ description: These metrics refer to the ZFS pool.
104
+ labels:
105
+ - name: pool
106
+ description: Zpool name
107
+ metrics:
108
+ - name: zfspool.pool_space_utilization
109
+ description: Zpool space utilization
110
+ unit: '%'
111
+ chart_type: area
112
+ dimensions:
113
+ - name: utilization
114
+ - name: zfspool.pool_space_usage
115
+ description: Zpool space usage
116
+ unit: 'bytes'
117
+ chart_type: stacked
118
+ dimensions:
119
+ - name: free
120
+ - name: used
121
+ - name: zfspool.pool_fragmentation
122
+ description: Zpool fragmentation
123
+ unit: '%'
124
+ chart_type: line
125
+ dimensions:
126
+ - name: fragmentation
127
+ - name: zfspool.pool_health_state
128
+ description: Zpool health state
129
+ unit: 'state'
130
+ chart_type: line
131
+ dimensions:
132
+ - name: online
133
+ - name: degraded
134
+ - name: faulted
135
+ - name: offline
136
+ - name: unavail
137
+ - name: removed
138
+ - name: suspended
src/go/collectors/go.d.plugin/modules/zfspool/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123,
4
+ "binary_path": "ok"
5
+}
src/go/collectors/go.d.plugin/modules/zfspool/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+timeout: 123.123
3
+binary_path: "ok"
src/go/collectors/go.d.plugin/modules/zfspool/testdata/zpool-list.txt
new
+3
@@ -0,0 +1,3 @@
1
+NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
2
+rpool 21367462298 9051643576 12240656794 - 33 42 1.00 ONLINE -
3
+zion - - - - - - - FAULTED -
src/go/collectors/go.d.plugin/modules/zfspool/zfspool.go
new
+111
@@ -0,0 +1,111 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
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("zfspool", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ })
25
+}
26
+
27
+func New() *ZFSPool {
28
+ return &ZFSPool{
29
+ Config: Config{
30
+ BinaryPath: "/usr/bin/zpool",
31
+ Timeout: web.Duration(time.Second * 2),
32
+ },
33
+ charts: &module.Charts{},
34
+ zpools: make(map[string]bool),
35
+ }
36
+}
37
+
38
+type Config struct {
39
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
40
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
41
+ BinaryPath string `yaml:"binary_path" json:"binary_path"`
42
+}
43
+
44
+type (
45
+ ZFSPool struct {
46
+ module.Base
47
+ Config `yaml:",inline" json:""`
48
+
49
+ charts *module.Charts
50
+
51
+ exec zpoolCLI
52
+
53
+ zpools map[string]bool
54
+ }
55
+ zpoolCLI interface {
56
+ list() ([]byte, error)
57
+ }
58
+)
59
+
60
+func (z *ZFSPool) Configuration() any {
61
+ return z.Config
62
+}
63
+
64
+func (z *ZFSPool) Init() error {
65
+ if err := z.validateConfig(); err != nil {
66
+ z.Errorf("config validation: %s", err)
67
+ return err
68
+ }
69
+
70
+ zpoolExec, err := z.initZPoolCLIExec()
71
+ if err != nil {
72
+ z.Errorf("zpool exec initialization: %v", err)
73
+ return err
74
+ }
75
+ z.exec = zpoolExec
76
+
77
+ return nil
78
+}
79
+
80
+func (z *ZFSPool) Check() error {
81
+ mx, err := z.collect()
82
+ if err != nil {
83
+ z.Error(err)
84
+ return err
85
+ }
86
+
87
+ if len(mx) == 0 {
88
+ return errors.New("no metrics collected")
89
+ }
90
+
91
+ return nil
92
+}
93
+
94
+func (z *ZFSPool) Charts() *module.Charts {
95
+ return z.charts
96
+}
97
+
98
+func (z *ZFSPool) Collect() map[string]int64 {
99
+ mx, err := z.collect()
100
+ if err != nil {
101
+ z.Error(err)
102
+ }
103
+
104
+ if len(mx) == 0 {
105
+ return nil
106
+ }
107
+
108
+ return mx
109
+}
110
+
111
+func (z *ZFSPool) Cleanup() {}
src/go/collectors/go.d.plugin/modules/zfspool/zfspool_test.go
new
+248
@@ -0,0 +1,248 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package zfspool
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
+ dataZpoolList, _ = os.ReadFile("testdata/zpool-list.txt")
21
+)
22
+
23
+func Test_testDataIsValid(t *testing.T) {
24
+ for name, data := range map[string][]byte{
25
+ "dataConfigJSON": dataConfigJSON,
26
+ "dataConfigYAML": dataConfigYAML,
27
+
28
+ "dataZpoolList": dataZpoolList,
29
+ } {
30
+ require.NotNil(t, data, name)
31
+
32
+ }
33
+}
34
+
35
+func TestZFSPool_Configuration(t *testing.T) {
36
+ module.TestConfigurationSerialize(t, &ZFSPool{}, dataConfigJSON, dataConfigYAML)
37
+}
38
+
39
+func TestZFSPool_Init(t *testing.T) {
40
+ tests := map[string]struct {
41
+ config Config
42
+ wantFail bool
43
+ }{
44
+ "fails if 'binary_path' is not set": {
45
+ wantFail: true,
46
+ config: Config{
47
+ BinaryPath: "",
48
+ },
49
+ },
50
+ "fails if failed to find binary": {
51
+ wantFail: true,
52
+ config: Config{
53
+ BinaryPath: "zpool!!!",
54
+ },
55
+ },
56
+ }
57
+
58
+ for name, test := range tests {
59
+ t.Run(name, func(t *testing.T) {
60
+ zp := New()
61
+ zp.Config = test.config
62
+
63
+ if test.wantFail {
64
+ assert.Error(t, zp.Init())
65
+ } else {
66
+ assert.NoError(t, zp.Init())
67
+ }
68
+ })
69
+ }
70
+
71
+}
72
+
73
+func TestZFSPool_Cleanup(t *testing.T) {
74
+ tests := map[string]struct {
75
+ prepare func() *ZFSPool
76
+ }{
77
+ "not initialized exec": {
78
+ prepare: func() *ZFSPool {
79
+ return New()
80
+ },
81
+ },
82
+ "after check": {
83
+ prepare: func() *ZFSPool {
84
+ zp := New()
85
+ zp.exec = prepareMockOK()
86
+ _ = zp.Check()
87
+ return zp
88
+ },
89
+ },
90
+ "after collect": {
91
+ prepare: func() *ZFSPool {
92
+ zp := New()
93
+ zp.exec = prepareMockOK()
94
+ _ = zp.Collect()
95
+ return zp
96
+ },
97
+ },
98
+ }
99
+
100
+ for name, test := range tests {
101
+ t.Run(name, func(t *testing.T) {
102
+ zp := test.prepare()
103
+
104
+ assert.NotPanics(t, zp.Cleanup)
105
+ })
106
+ }
107
+}
108
+
109
+func TestZFSPool_Charts(t *testing.T) {
110
+ assert.NotNil(t, New().Charts())
111
+}
112
+
113
+func TestZFSPool_Check(t *testing.T) {
114
+ tests := map[string]struct {
115
+ prepareMock func() *mockZpoolCLIExec
116
+ wantFail bool
117
+ }{
118
+ "success case": {
119
+ prepareMock: prepareMockOK,
120
+ wantFail: false,
121
+ },
122
+ "error on list call": {
123
+ prepareMock: prepareMockErrOnList,
124
+ wantFail: true,
125
+ },
126
+ "empty response": {
127
+ prepareMock: prepareMockEmptyResponse,
128
+ wantFail: true,
129
+ },
130
+ "unexpected response": {
131
+ prepareMock: prepareMockUnexpectedResponse,
132
+ wantFail: true,
133
+ },
134
+ }
135
+
136
+ for name, test := range tests {
137
+ t.Run(name, func(t *testing.T) {
138
+ zp := New()
139
+ mock := test.prepareMock()
140
+ zp.exec = mock
141
+
142
+ if test.wantFail {
143
+ assert.Error(t, zp.Check())
144
+ } else {
145
+ assert.NoError(t, zp.Check())
146
+ }
147
+ })
148
+ }
149
+}
150
+
151
+func TestZFSPool_Collect(t *testing.T) {
152
+ tests := map[string]struct {
153
+ prepareMock func() *mockZpoolCLIExec
154
+ wantMetrics map[string]int64
155
+ }{
156
+ "success case": {
157
+ prepareMock: prepareMockOK,
158
+ wantMetrics: map[string]int64{
159
+ "zpool_rpool_alloc": 9051643576,
160
+ "zpool_rpool_cap": 42,
161
+ "zpool_rpool_frag": 33,
162
+ "zpool_rpool_free": 12240656794,
163
+ "zpool_rpool_health_state_degraded": 0,
164
+ "zpool_rpool_health_state_faulted": 0,
165
+ "zpool_rpool_health_state_offline": 0,
166
+ "zpool_rpool_health_state_online": 1,
167
+ "zpool_rpool_health_state_removed": 0,
168
+ "zpool_rpool_health_state_suspended": 0,
169
+ "zpool_rpool_health_state_unavail": 0,
170
+ "zpool_rpool_size": 21367462298,
171
+ "zpool_zion_health_state_degraded": 0,
172
+ "zpool_zion_health_state_faulted": 1,
173
+ "zpool_zion_health_state_offline": 0,
174
+ "zpool_zion_health_state_online": 0,
175
+ "zpool_zion_health_state_removed": 0,
176
+ "zpool_zion_health_state_suspended": 0,
177
+ "zpool_zion_health_state_unavail": 0,
178
+ },
179
+ },
180
+ "error on list call": {
181
+ prepareMock: prepareMockErrOnList,
182
+ wantMetrics: nil,
183
+ },
184
+ "empty response": {
185
+ prepareMock: prepareMockEmptyResponse,
186
+ wantMetrics: nil,
187
+ },
188
+ "unexpected response": {
189
+ prepareMock: prepareMockUnexpectedResponse,
190
+ wantMetrics: nil,
191
+ },
192
+ }
193
+
194
+ for name, test := range tests {
195
+ t.Run(name, func(t *testing.T) {
196
+ zp := New()
197
+ mock := test.prepareMock()
198
+ zp.exec = mock
199
+
200
+ mx := zp.Collect()
201
+
202
+ assert.Equal(t, test.wantMetrics, mx)
203
+ if len(test.wantMetrics) > 0 {
204
+ assert.Len(t, *zp.Charts(), len(zpoolChartsTmpl)*len(zp.zpools))
205
+ }
206
+ })
207
+ }
208
+
209
+}
210
+
211
+func prepareMockOK() *mockZpoolCLIExec {
212
+ return &mockZpoolCLIExec{
213
+ listData: dataZpoolList,
214
+ }
215
+}
216
+
217
+func prepareMockErrOnList() *mockZpoolCLIExec {
218
+ return &mockZpoolCLIExec{
219
+ errOnList: true,
220
+ }
221
+}
222
+
223
+func prepareMockEmptyResponse() *mockZpoolCLIExec {
224
+ return &mockZpoolCLIExec{}
225
+}
226
+
227
+func prepareMockUnexpectedResponse() *mockZpoolCLIExec {
228
+ return &mockZpoolCLIExec{
229
+ listData: []byte(`
230
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
231
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
232
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
233
+`),
234
+ }
235
+}
236
+
237
+type mockZpoolCLIExec struct {
238
+ errOnList bool
239
+ listData []byte
240
+}
241
+
242
+func (m *mockZpoolCLIExec) list() ([]byte, error) {
243
+ if m.errOnList {
244
+ return nil, errors.New("mock.list() error")
245
+ }
246
+
247
+ return m.listData, nil
248
+}
src/health/health.d/zfs.conf
+46
@@ -42,3 +42,49 @@ component: File system
42
summary: Critical ZFS pool ${label:pool} state
43
info: ZFS pool ${label:pool} state is faulted or unavail
44
to: sysadmin
45
+
46
+
47
+## go.d/zfspool
48
+
49
+ template: zfs_pool_space_utilization
50
+ on: zfspool.pool_space_utilization
51
+ class: Utilization
52
+ type: System
53
+component: File system
54
+ calc: $utilization
55
+ units: %
56
+ every: 1m
57
+ warn: $this > (($status >= $WARNING ) ? (85) : (90))
58
+ crit: $this > (($status >= $WARNING ) ? (90) : (98))
59
+ delay: down 1m multiplier 1.5 max 1h
60
+ summary: ZFS pool ${label:pool} space utilization
61
+ info: ZFS pool ${label:pool} is nearing capacity. Current space usage is above the threshold.
62
+ to: sysadmin
63
+
64
+ template: zfs_pool_health_state_warn
65
+ on: zfspool.pool_health_state
66
+ class: Errors
67
+ type: System
68
+component: File system
69
+ calc: $degraded
70
+ units: boolean
71
+ every: 10s
72
+ warn: $this > 0
73
+ delay: down 1m multiplier 1.5 max 1h
74
+ summary: ZFS pool ${label:pool} state
75
+ info: ZFS pool ${label:pool} state is degraded
76
+ to: sysadmin
77
+
78
+ template: zfs_pool_health_state_crit
79
+ on: zfspool.pool_health_state
80
+ class: Errors
81
+ type: System
82
+component: File system
83
+ calc: $faulted + $unavail
84
+ units: boolean
85
+ every: 10s
86
+ crit: $this > 0
87
+ delay: down 1m multiplier 1.5 max 1h
88
+ summary: Critical ZFS pool ${label:pool} state
89
+ info: ZFS pool ${label:pool} state is faulted or unavail
90
+ to: sysadmin