@cryptotaxi247 / netdata-1 / commits / 7b4b25887

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