summaryrefslogtreecommitdiffstats
path: root/src/go/plugin/go.d/modules/zfspool
diff options
context:
space:
mode:
Diffstat (limited to 'src/go/plugin/go.d/modules/zfspool')
l---------src/go/plugin/go.d/modules/zfspool/README.md1
-rw-r--r--src/go/plugin/go.d/modules/zfspool/charts.go175
-rw-r--r--src/go/plugin/go.d/modules/zfspool/collect.go27
-rw-r--r--src/go/plugin/go.d/modules/zfspool/collect_zpool_list.go160
-rw-r--r--src/go/plugin/go.d/modules/zfspool/collect_zpool_list_vdev.go138
-rw-r--r--src/go/plugin/go.d/modules/zfspool/config_schema.json47
-rw-r--r--src/go/plugin/go.d/modules/zfspool/exec.go56
-rw-r--r--src/go/plugin/go.d/modules/zfspool/init.go38
-rw-r--r--src/go/plugin/go.d/modules/zfspool/integrations/zfs_pools.md222
-rw-r--r--src/go/plugin/go.d/modules/zfspool/metadata.yaml162
-rw-r--r--src/go/plugin/go.d/modules/zfspool/testdata/config.json5
-rw-r--r--src/go/plugin/go.d/modules/zfspool/testdata/config.yaml3
-rw-r--r--src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev-logs-cache.txt12
-rw-r--r--src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev.txt5
-rw-r--r--src/go/plugin/go.d/modules/zfspool/testdata/zpool-list.txt3
-rw-r--r--src/go/plugin/go.d/modules/zfspool/zfspool.go115
-rw-r--r--src/go/plugin/go.d/modules/zfspool/zfspool_test.go546
17 files changed, 1715 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/zfspool/README.md b/src/go/plugin/go.d/modules/zfspool/README.md
new file mode 120000
index 00000000..8a292336
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/README.md
@@ -0,0 +1 @@
+integrations/zfs_pools.md \ No newline at end of file
diff --git a/src/go/plugin/go.d/modules/zfspool/charts.go b/src/go/plugin/go.d/modules/zfspool/charts.go
new file mode 100644
index 00000000..92a7d53b
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/charts.go
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
+)
+
+const (
+ prioZpoolHealthState = 2820 + iota
+ prioVdevHealthState
+
+ prioZpoolSpaceUtilization
+ prioZpoolSpaceUsage
+
+ prioZpoolFragmentation
+)
+
+var zpoolChartsTmpl = module.Charts{
+ zpoolHealthStateChartTmpl.Copy(),
+
+ zpoolSpaceUtilizationChartTmpl.Copy(),
+ zpoolSpaceUsageChartTmpl.Copy(),
+
+ zpoolFragmentationChartTmpl.Copy(),
+}
+
+var (
+ zpoolHealthStateChartTmpl = module.Chart{
+ ID: "zfspool_%s_health_state",
+ Title: "Zpool health state",
+ Units: "state",
+ Fam: "health",
+ Ctx: "zfspool.pool_health_state",
+ Type: module.Line,
+ Priority: prioZpoolHealthState,
+ Dims: module.Dims{
+ {ID: "zpool_%s_health_state_online", Name: "online"},
+ {ID: "zpool_%s_health_state_degraded", Name: "degraded"},
+ {ID: "zpool_%s_health_state_faulted", Name: "faulted"},
+ {ID: "zpool_%s_health_state_offline", Name: "offline"},
+ {ID: "zpool_%s_health_state_unavail", Name: "unavail"},
+ {ID: "zpool_%s_health_state_removed", Name: "removed"},
+ {ID: "zpool_%s_health_state_suspended", Name: "suspended"},
+ },
+ }
+
+ zpoolSpaceUtilizationChartTmpl = module.Chart{
+ ID: "zfspool_%s_space_utilization",
+ Title: "Zpool space utilization",
+ Units: "percentage",
+ Fam: "space usage",
+ Ctx: "zfspool.pool_space_utilization",
+ Type: module.Area,
+ Priority: prioZpoolSpaceUtilization,
+ Dims: module.Dims{
+ {ID: "zpool_%s_cap", Name: "utilization"},
+ },
+ }
+ zpoolSpaceUsageChartTmpl = module.Chart{
+ ID: "zfspool_%s_space_usage",
+ Title: "Zpool space usage",
+ Units: "bytes",
+ Fam: "space usage",
+ Ctx: "zfspool.pool_space_usage",
+ Type: module.Stacked,
+ Priority: prioZpoolSpaceUsage,
+ Dims: module.Dims{
+ {ID: "zpool_%s_free", Name: "free"},
+ {ID: "zpool_%s_alloc", Name: "used"},
+ },
+ }
+
+ zpoolFragmentationChartTmpl = module.Chart{
+ ID: "zfspool_%s_fragmentation",
+ Title: "Zpool fragmentation",
+ Units: "percentage",
+ Fam: "fragmentation",
+ Ctx: "zfspool.pool_fragmentation",
+ Type: module.Line,
+ Priority: prioZpoolFragmentation,
+ Dims: module.Dims{
+ {ID: "zpool_%s_frag", Name: "fragmentation"},
+ },
+ }
+)
+
+var vdevChartsTmpl = module.Charts{
+ vdevHealthStateChartTmpl.Copy(),
+}
+
+var (
+ vdevHealthStateChartTmpl = module.Chart{
+ ID: "vdev_%s_health_state",
+ Title: "Zpool Vdev health state",
+ Units: "state",
+ Fam: "health",
+ Ctx: "zfspool.vdev_health_state",
+ Type: module.Line,
+ Priority: prioVdevHealthState,
+ Dims: module.Dims{
+ {ID: "vdev_%s_health_state_online", Name: "online"},
+ {ID: "vdev_%s_health_state_degraded", Name: "degraded"},
+ {ID: "vdev_%s_health_state_faulted", Name: "faulted"},
+ {ID: "vdev_%s_health_state_offline", Name: "offline"},
+ {ID: "vdev_%s_health_state_unavail", Name: "unavail"},
+ {ID: "vdev_%s_health_state_removed", Name: "removed"},
+ {ID: "vdev_%s_health_state_suspended", Name: "suspended"},
+ },
+ }
+)
+
+func (z *ZFSPool) addZpoolCharts(name string) {
+ charts := zpoolChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, name)
+ chart.Labels = []module.Label{
+ {Key: "pool", Value: name},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, name)
+ }
+ }
+
+ if err := z.Charts().Add(*charts...); err != nil {
+ z.Warning(err)
+ }
+}
+
+func (z *ZFSPool) removeZpoolCharts(name string) {
+ px := fmt.Sprintf("zfspool_%s_", name)
+ z.removeCharts(px)
+}
+
+func (z *ZFSPool) addVdevCharts(pool, vdev string) {
+ charts := vdevChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, cleanVdev(vdev))
+ chart.Labels = []module.Label{
+ {Key: "pool", Value: pool},
+ {Key: "vdev", Value: vdev},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, vdev)
+ }
+ }
+
+ if err := z.Charts().Add(*charts...); err != nil {
+ z.Warning(err)
+ }
+}
+
+func (z *ZFSPool) removeVdevCharts(vdev string) {
+ px := fmt.Sprintf("vdev_%s_", cleanVdev(vdev))
+ z.removeCharts(px)
+}
+
+func (z *ZFSPool) removeCharts(px string) {
+ for _, chart := range *z.Charts() {
+ if strings.HasPrefix(chart.ID, px) {
+ chart.MarkRemove()
+ chart.MarkNotCreated()
+ }
+ }
+}
+
+func cleanVdev(vdev string) string {
+ r := strings.NewReplacer(".", "_")
+ return r.Replace(vdev)
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/collect.go b/src/go/plugin/go.d/modules/zfspool/collect.go
new file mode 100644
index 00000000..b9b29058
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/collect.go
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+var zpoolHealthStates = []string{
+ "online",
+ "degraded",
+ "faulted",
+ "offline",
+ "removed",
+ "unavail",
+ "suspended",
+}
+
+func (z *ZFSPool) collect() (map[string]int64, error) {
+
+ mx := make(map[string]int64)
+
+ if err := z.collectZpoolList(mx); err != nil {
+ return nil, err
+ }
+ if err := z.collectZpoolListVdev(mx); err != nil {
+ return mx, err
+ }
+
+ return mx, nil
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/collect_zpool_list.go b/src/go/plugin/go.d/modules/zfspool/collect_zpool_list.go
new file mode 100644
index 00000000..f5e1c081
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/collect_zpool_list.go
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type zpoolEntry struct {
+ name string
+ sizeBytes string
+ allocBytes string
+ freeBytes string
+ fragPerc string
+ capPerc string
+ dedupRatio string
+ health string
+}
+
+func (z *ZFSPool) collectZpoolList(mx map[string]int64) error {
+ bs, err := z.exec.list()
+ if err != nil {
+ return err
+ }
+
+ zpools, err := parseZpoolListOutput(bs)
+ if err != nil {
+ return fmt.Errorf("bad zpool list output: %v", err)
+ }
+
+ seen := make(map[string]bool)
+
+ for _, zpool := range zpools {
+ seen[zpool.name] = true
+
+ if !z.seenZpools[zpool.name] {
+ z.addZpoolCharts(zpool.name)
+ z.seenZpools[zpool.name] = true
+ }
+
+ px := "zpool_" + zpool.name + "_"
+
+ if v, ok := parseInt(zpool.sizeBytes); ok {
+ mx[px+"size"] = v
+ }
+ if v, ok := parseInt(zpool.freeBytes); ok {
+ mx[px+"free"] = v
+ }
+ if v, ok := parseInt(zpool.allocBytes); ok {
+ mx[px+"alloc"] = v
+ }
+ if v, ok := parseFloat(zpool.capPerc); ok {
+ mx[px+"cap"] = int64(v)
+ }
+ if v, ok := parseFloat(zpool.fragPerc); ok {
+ mx[px+"frag"] = int64(v)
+ }
+ for _, s := range zpoolHealthStates {
+ mx[px+"health_state_"+s] = 0
+ }
+ mx[px+"health_state_"+zpool.health] = 1
+ }
+
+ for name := range z.seenZpools {
+ if !seen[name] {
+ z.removeZpoolCharts(name)
+ delete(z.seenZpools, name)
+ }
+ }
+
+ return nil
+}
+
+func parseZpoolListOutput(bs []byte) ([]zpoolEntry, error) {
+ /*
+ # zpool list -p
+ NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
+ rpool 21367462298 9051643576 12240656794 - 33 42 1.00 ONLINE -
+ zion - - - - - - - FAULTED -
+ */
+
+ var headers []string
+ var zpools []zpoolEntry
+ sc := bufio.NewScanner(bytes.NewReader(bs))
+
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+ if line == "" {
+ continue
+ }
+
+ if len(headers) == 0 {
+ if !strings.HasPrefix(line, "NAME") {
+ return nil, fmt.Errorf("missing headers (line '%s')", line)
+ }
+ headers = strings.Fields(line)
+ continue
+ }
+
+ values := strings.Fields(line)
+ if len(values) != len(headers) {
+ return nil, fmt.Errorf("unequal columns: headers(%d) != values(%d)", len(headers), len(values))
+ }
+
+ var zpool zpoolEntry
+
+ for i, v := range values {
+ v = strings.TrimSpace(v)
+ switch strings.ToLower(headers[i]) {
+ case "name":
+ zpool.name = v
+ case "size":
+ zpool.sizeBytes = v
+ case "alloc":
+ zpool.allocBytes = v
+ case "free":
+ zpool.freeBytes = v
+ case "frag":
+ zpool.fragPerc = v
+ case "cap":
+ zpool.capPerc = v
+ case "dedup":
+ zpool.dedupRatio = v
+ case "health":
+ zpool.health = strings.ToLower(v)
+ }
+ }
+
+ if zpool.name != "" && zpool.health != "" {
+ zpools = append(zpools, zpool)
+ }
+ }
+
+ if len(zpools) == 0 {
+ return nil, errors.New("no pools found")
+ }
+
+ return zpools, nil
+}
+
+func parseInt(s string) (int64, bool) {
+ if s == "-" {
+ return 0, false
+ }
+ v, err := strconv.ParseInt(s, 10, 64)
+ return v, err == nil
+}
+
+func parseFloat(s string) (float64, bool) {
+ if s == "-" {
+ return 0, false
+ }
+ v, err := strconv.ParseFloat(s, 64)
+ return v, err == nil
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/collect_zpool_list_vdev.go b/src/go/plugin/go.d/modules/zfspool/collect_zpool_list_vdev.go
new file mode 100644
index 00000000..30e1fe4e
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/collect_zpool_list_vdev.go
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+type vdevEntry struct {
+ name string
+ vdev string // The full path of the vdev within the zpool hierarchy.
+ health string
+
+ // Represents the nesting level of the vdev within the zpool hierarchy, based on indentation.
+ // A level of -1 indicates the root vdev (the pool itself).
+ level int
+}
+
+func (z *ZFSPool) collectZpoolListVdev(mx map[string]int64) error {
+ seen := make(map[string]bool)
+
+ for pool := range z.seenZpools {
+ bs, err := z.exec.listWithVdev(pool)
+ if err != nil {
+ return err
+ }
+
+ vdevs, err := parseZpoolListVdevOutput(bs)
+ if err != nil {
+ return fmt.Errorf("bad zpool list vdev output (pool '%s'): %v", pool, err)
+ }
+
+ for _, vdev := range vdevs {
+ if vdev.health == "" || vdev.health == "-" {
+ continue
+ }
+
+ seen[vdev.vdev] = true
+ if !z.seenVdevs[vdev.vdev] {
+ z.seenVdevs[vdev.vdev] = true
+ z.addVdevCharts(pool, vdev.vdev)
+ }
+
+ px := fmt.Sprintf("vdev_%s_", vdev.vdev)
+
+ for _, s := range zpoolHealthStates {
+ mx[px+"health_state_"+s] = 0
+ }
+ mx[px+"health_state_"+vdev.health] = 1
+ }
+ }
+
+ for name := range z.seenVdevs {
+ if !seen[name] {
+ z.removeVdevCharts(name)
+ delete(z.seenVdevs, name)
+ }
+ }
+
+ return nil
+}
+
+func parseZpoolListVdevOutput(bs []byte) ([]vdevEntry, error) {
+ var headers []string
+ var vdevs []vdevEntry
+ sc := bufio.NewScanner(bytes.NewReader(bs))
+
+ for sc.Scan() {
+ line := sc.Text()
+ if line == "" {
+ continue
+ }
+
+ if len(headers) == 0 {
+ if !strings.HasPrefix(line, "NAME") {
+ return nil, fmt.Errorf("missing headers (line '%s')", line)
+ }
+ headers = strings.Fields(line)
+ continue
+ }
+
+ values := strings.Fields(line)
+ if len(values) == 0 || len(values) > len(headers) {
+ return nil, fmt.Errorf("unexpected columns: headers(%d) values(%d) (line '%s')", len(headers), len(values), line)
+ }
+
+ vdev := vdevEntry{
+ level: len(line) - len(strings.TrimLeft(line, " ")),
+ }
+
+ for i, v := range values {
+ switch strings.ToLower(headers[i]) {
+ case "name":
+ vdev.name = v
+ case "health":
+ vdev.health = strings.ToLower(v)
+ }
+ }
+
+ if vdev.name != "" {
+ if len(vdevs) == 0 {
+ vdev.level = -1 // Pool
+ }
+ vdevs = append(vdevs, vdev)
+ }
+ }
+
+ // set parent/child relationships
+ for i := range vdevs {
+ v := &vdevs[i]
+
+ switch i {
+ case 0:
+ v.vdev = v.name
+ default:
+ // find parent with a lower level
+ for j := i - 1; j >= 0; j-- {
+ if vdevs[j].level < v.level {
+ v.vdev = fmt.Sprintf("%s/%s", vdevs[j].vdev, v.name)
+ break
+ }
+ }
+ if v.vdev == "" {
+ return nil, fmt.Errorf("no parent for vdev '%s'", v.name)
+ }
+ }
+ }
+
+ // first is Pool
+ if len(vdevs) < 2 {
+ return nil, fmt.Errorf("no vdevs found")
+ }
+
+ return vdevs[1:], nil
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/config_schema.json b/src/go/plugin/go.d/modules/zfspool/config_schema.json
new file mode 100644
index 00000000..fcfcff1d
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/config_schema.json
@@ -0,0 +1,47 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "ZFS Pools collector configuration",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 10
+ },
+ "binary_path": {
+ "title": "Binary path",
+ "description": "Path to the `zpool` binary.",
+ "type": "string",
+ "default": "/usr/bin/zpool"
+ },
+ "timeout": {
+ "title": "Timeout",
+ "description": "Timeout for executing the binary, specified in seconds.",
+ "type": "number",
+ "minimum": 0.5,
+ "default": 2
+ }
+ },
+ "required": [
+ "binary_path"
+ ],
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "binary_path": {
+ "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."
+ },
+ "timeout": {
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
+ }
+ }
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/exec.go b/src/go/plugin/go.d/modules/zfspool/exec.go
new file mode 100644
index 00000000..1a2bcf20
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/exec.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+ "time"
+
+ "github.com/netdata/netdata/go/plugins/logger"
+)
+
+func newZpoolCLIExec(binPath string, timeout time.Duration) *zpoolCLIExec {
+ return &zpoolCLIExec{
+ binPath: binPath,
+ timeout: timeout,
+ }
+}
+
+type zpoolCLIExec struct {
+ *logger.Logger
+
+ binPath string
+ timeout time.Duration
+}
+
+func (e *zpoolCLIExec) list() ([]byte, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, e.binPath, "list", "-p")
+ e.Debugf("executing '%s'", cmd)
+
+ bs, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
+ }
+
+ return bs, nil
+}
+
+func (e *zpoolCLIExec) listWithVdev(pool string) ([]byte, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, e.binPath, "list", "-p", "-v", "-L", pool)
+ e.Debugf("executing '%s'", cmd)
+
+ bs, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
+ }
+
+ return bs, nil
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/init.go b/src/go/plugin/go.d/modules/zfspool/init.go
new file mode 100644
index 00000000..f640801d
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/init.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+func (z *ZFSPool) validateConfig() error {
+ if z.BinaryPath == "" {
+ return errors.New("no zpool binary path specified")
+ }
+ return nil
+}
+
+func (z *ZFSPool) initZPoolCLIExec() (zpoolCLI, error) {
+ binPath := z.BinaryPath
+
+ if !strings.HasPrefix(binPath, "/") {
+ path, err := exec.LookPath(binPath)
+ if err != nil {
+ return nil, err
+ }
+ binPath = path
+ }
+
+ if _, err := os.Stat(binPath); err != nil {
+ return nil, err
+ }
+
+ zpoolExec := newZpoolCLIExec(binPath, z.Timeout.Duration())
+ zpoolExec.Logger = z.Logger
+
+ return zpoolExec, nil
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/integrations/zfs_pools.md b/src/go/plugin/go.d/modules/zfspool/integrations/zfs_pools.md
new file mode 100644
index 00000000..060e4fb7
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/integrations/zfs_pools.md
@@ -0,0 +1,222 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/zfspool/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/zfspool/metadata.yaml"
+sidebar_label: "ZFS Pools"
+learn_status: "Published"
+learn_rel_path: "Collecting Metrics/Storage, Mount Points and Filesystems"
+most_popular: False
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
+endmeta-->
+
+# ZFS Pools
+
+
+<img src="https://netdata.cloud/img/filesystem.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: zfspool
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This collector monitors the health and space usage of ZFS pools using the command line tool [zpool](https://openzfs.github.io/openzfs-docs/man/master/8/zpool-list.8.html).
+
+
+
+
+This collector is supported on all platforms.
+
+This collector only supports collecting metrics from a single instance of this integration.
+
+
+### Default Behavior
+
+#### Auto-Detection
+
+This integration doesn't support auto-detection.
+
+#### Limits
+
+The default configuration for this integration does not impose any limits on data collection.
+
+#### Performance Impact
+
+The default configuration for this integration is not expected to impose a significant performance impact on the system.
+
+
+## Metrics
+
+Metrics grouped by *scope*.
+
+The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
+
+
+
+### Per zfs pool
+
+These metrics refer to the ZFS pool.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| pool | Zpool name |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| zfspool.pool_space_utilization | utilization | % |
+| zfspool.pool_space_usage | free, used | bytes |
+| zfspool.pool_fragmentation | fragmentation | % |
+| zfspool.pool_health_state | online, degraded, faulted, offline, unavail, removed, suspended | state |
+
+### Per zfs pool vdev
+
+These metrics refer to the ZFS pool virtual device.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| pool | Zpool name |
+| vdev | Unique identifier for a virtual device (vdev) within a ZFS pool. |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| zfspool.vdev_health_state | online, degraded, faulted, offline, unavail, removed, suspended | state |
+
+
+
+## Alerts
+
+
+The following alerts are available:
+
+| Alert name | On metric | Description |
+|:------------|:----------|:------------|
+| [ zfs_pool_space_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf) | zfspool.pool_space_utilization | ZFS pool ${label:pool} is nearing capacity. Current space usage is above the threshold. |
+| [ zfs_pool_health_state_warn ](https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf) | zfspool.pool_health_state | ZFS pool ${label:pool} state is degraded |
+| [ zfs_pool_health_state_crit ](https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf) | zfspool.pool_health_state | ZFS pool ${label:pool} state is faulted or unavail |
+| [ zfs_vdev_health_state ](https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf) | zfspool.vdev_health_state | ZFS vdev ${label:vdev} state is faulted or degraded |
+
+
+## Setup
+
+### Prerequisites
+
+No action required.
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/zfspool.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/zfspool.conf
+```
+#### Options
+
+The following options can be defined globally: update_every.
+
+
+<details open><summary>Config options</summary>
+
+| Name | Description | Default | Required |
+|:----|:-----------|:-------|:--------:|
+| update_every | Data collection frequency. | 10 | no |
+| binary_path | 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. | /usr/bin/zpool | yes |
+| timeout | Timeout for executing the binary, specified in seconds. | 2 | no |
+
+</details>
+
+#### Examples
+
+##### Custom binary path
+
+The executable is not in the directories specified in the PATH environment variable.
+
+<details open><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: zfspool
+ binary_path: /usr/local/sbin/zpool
+
+```
+</details>
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature.
+
+To troubleshoot issues with the `zfspool` collector, run the `go.d.plugin` with the debug option enabled. The output
+should give you clues as to why the collector isn't working.
+
+- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
+ your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
+
+ ```bash
+ cd /usr/libexec/netdata/plugins.d/
+ ```
+
+- Switch to the `netdata` user.
+
+ ```bash
+ sudo -u netdata -s
+ ```
+
+- Run the `go.d.plugin` to debug the collector:
+
+ ```bash
+ ./go.d.plugin -d -m zfspool
+ ```
+
+### Getting Logs
+
+If you're encountering problems with the `zfspool` collector, follow these steps to retrieve logs and identify potential issues:
+
+- **Run the command** specific to your system (systemd, non-systemd, or Docker container).
+- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem.
+
+#### System with systemd
+
+Use the following command to view logs generated since the last Netdata service restart:
+
+```bash
+journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep zfspool
+```
+
+#### System without systemd
+
+Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name:
+
+```bash
+grep zfspool /var/log/netdata/collector.log
+```
+
+**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues.
+
+#### Docker Container
+
+If your Netdata runs in a Docker container named "netdata" (replace if different), use this command:
+
+```bash
+docker logs netdata 2>&1 | grep zfspool
+```
+
+
diff --git a/src/go/plugin/go.d/modules/zfspool/metadata.yaml b/src/go/plugin/go.d/modules/zfspool/metadata.yaml
new file mode 100644
index 00000000..21cc307c
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/metadata.yaml
@@ -0,0 +1,162 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-zfspool
+ plugin_name: go.d.plugin
+ module_name: zfspool
+ monitored_instance:
+ name: ZFS Pools
+ link: ""
+ icon_filename: filesystem.svg
+ categories:
+ - data-collection.storage-mount-points-and-filesystems
+ keywords:
+ - zfs pools
+ - pools
+ - zfs
+ - filesystem
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: >
+ This collector monitors the health and space usage of ZFS pools using the command line
+ tool [zpool](https://openzfs.github.io/openzfs-docs/man/master/8/zpool-list.8.html).
+ method_description: ""
+ supported_platforms:
+ include: []
+ exclude: []
+ multi_instance: false
+ additional_permissions:
+ description: ""
+ default_behavior:
+ auto_detection:
+ description: ""
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/zfspool.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 10
+ required: false
+ - name: binary_path
+ 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.
+ default_value: /usr/bin/zpool
+ required: true
+ - name: timeout
+ description: Timeout for executing the binary, specified in seconds.
+ default_value: 2
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list:
+ - name: Custom binary path
+ description: The executable is not in the directories specified in the PATH environment variable.
+ config: |
+ jobs:
+ - name: zfspool
+ binary_path: /usr/local/sbin/zpool
+ troubleshooting:
+ problems:
+ list: []
+ alerts:
+ - name: zfs_pool_space_utilization
+ metric: zfspool.pool_space_utilization
+ info: "ZFS pool ${label:pool} is nearing capacity. Current space usage is above the threshold."
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
+ - name: zfs_pool_health_state_warn
+ metric: zfspool.pool_health_state
+ info: "ZFS pool ${label:pool} state is degraded"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
+ - name: zfs_pool_health_state_crit
+ metric: zfspool.pool_health_state
+ info: "ZFS pool ${label:pool} state is faulted or unavail"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
+ - name: zfs_vdev_health_state
+ metric: zfspool.vdev_health_state
+ info: "ZFS vdev ${label:vdev} state is faulted or degraded"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/zfs.conf
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: zfs pool
+ description: These metrics refer to the ZFS pool.
+ labels:
+ - name: pool
+ description: Zpool name
+ metrics:
+ - name: zfspool.pool_space_utilization
+ description: Zpool space utilization
+ unit: '%'
+ chart_type: area
+ dimensions:
+ - name: utilization
+ - name: zfspool.pool_space_usage
+ description: Zpool space usage
+ unit: 'bytes'
+ chart_type: stacked
+ dimensions:
+ - name: free
+ - name: used
+ - name: zfspool.pool_fragmentation
+ description: Zpool fragmentation
+ unit: '%'
+ chart_type: line
+ dimensions:
+ - name: fragmentation
+ - name: zfspool.pool_health_state
+ description: Zpool health state
+ unit: 'state'
+ chart_type: line
+ dimensions:
+ - name: online
+ - name: degraded
+ - name: faulted
+ - name: offline
+ - name: unavail
+ - name: removed
+ - name: suspended
+ - name: zfs pool vdev
+ description: These metrics refer to the ZFS pool virtual device.
+ labels:
+ - name: pool
+ description: Zpool name
+ - name: vdev
+ description: Unique identifier for a virtual device (vdev) within a ZFS pool.
+ metrics:
+ - name: zfspool.vdev_health_state
+ description: Zpool Vdev health state
+ unit: 'state'
+ chart_type: line
+ dimensions:
+ - name: online
+ - name: degraded
+ - name: faulted
+ - name: offline
+ - name: unavail
+ - name: removed
+ - name: suspended
diff --git a/src/go/plugin/go.d/modules/zfspool/testdata/config.json b/src/go/plugin/go.d/modules/zfspool/testdata/config.json
new file mode 100644
index 00000000..09571319
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/testdata/config.json
@@ -0,0 +1,5 @@
+{
+ "update_every": 123,
+ "timeout": 123.123,
+ "binary_path": "ok"
+}
diff --git a/src/go/plugin/go.d/modules/zfspool/testdata/config.yaml b/src/go/plugin/go.d/modules/zfspool/testdata/config.yaml
new file mode 100644
index 00000000..baf3bcd0
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/testdata/config.yaml
@@ -0,0 +1,3 @@
+update_every: 123
+timeout: 123.123
+binary_path: "ok"
diff --git a/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev-logs-cache.txt b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev-logs-cache.txt
new file mode 100644
index 00000000..061ca6cc
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev-logs-cache.txt
@@ -0,0 +1,12 @@
+NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
+rpool 9981503995904 3046188658688 6935315337216 - - 9 30 1.00 DEGRADED -
+ mirror-0 9981503995904 3046188658688 6935315337216 - - 9 30 - ONLINE
+ sdc2 9998683602944 - - - - - - - ONLINE
+ sdd2 9998683602944 - - - - - - - ONLINE
+logs - - - - - - - - -
+ mirror-1 17716740096 393216 17716346880 - - 0 0 - DEGRADED
+ sdb1 17951621120 - - - - - - - ONLINE
+ 14807975228228307538 - - - - - - - - UNAVAIL
+cache - - - - - - - - -
+ sdb2 99000254464 98755866624 239665152 - - 0 99 - ONLINE
+ wwn-0x500151795954c095-part2 - - - - - - - - UNAVAIL
diff --git a/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev.txt b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev.txt
new file mode 100644
index 00000000..ff78f8df
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list-vdev.txt
@@ -0,0 +1,5 @@
+NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
+rpool 3985729650688 1647130456064 2338599194624 - - 55 41 1.00 ONLINE -
+ mirror-0 3985729650688 1647130456064 2338599194624 - - 55 41 - ONLINE
+ nvme2n1p3 4000249020416 - - - - - - - ONLINE
+ nvme0n1p3 4000249020416 - - - - - - - ONLINE
diff --git a/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list.txt b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list.txt
new file mode 100644
index 00000000..06d9915c
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/testdata/zpool-list.txt
@@ -0,0 +1,3 @@
+NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
+rpool 21367462298 9051643576 12240656794 - 33 42 1.00 ONLINE -
+zion - - - - - - - FAULTED -
diff --git a/src/go/plugin/go.d/modules/zfspool/zfspool.go b/src/go/plugin/go.d/modules/zfspool/zfspool.go
new file mode 100644
index 00000000..02f1f7ce
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/zfspool.go
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("zfspool", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 10,
+ },
+ Create: func() module.Module { return New() },
+ Config: func() any { return &Config{} },
+ })
+}
+
+func New() *ZFSPool {
+ return &ZFSPool{
+ Config: Config{
+ BinaryPath: "/usr/bin/zpool",
+ Timeout: web.Duration(time.Second * 2),
+ },
+ charts: &module.Charts{},
+ seenZpools: make(map[string]bool),
+ seenVdevs: make(map[string]bool),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
+ BinaryPath string `yaml:"binary_path,omitempty" json:"binary_path"`
+}
+
+type (
+ ZFSPool struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ exec zpoolCLI
+
+ seenZpools map[string]bool
+ seenVdevs map[string]bool
+ }
+ zpoolCLI interface {
+ list() ([]byte, error)
+ listWithVdev(pool string) ([]byte, error)
+ }
+)
+
+func (z *ZFSPool) Configuration() any {
+ return z.Config
+}
+
+func (z *ZFSPool) Init() error {
+ if err := z.validateConfig(); err != nil {
+ z.Errorf("config validation: %s", err)
+ return err
+ }
+
+ zpoolExec, err := z.initZPoolCLIExec()
+ if err != nil {
+ z.Errorf("zpool exec initialization: %v", err)
+ return err
+ }
+ z.exec = zpoolExec
+
+ return nil
+}
+
+func (z *ZFSPool) Check() error {
+ mx, err := z.collect()
+ if err != nil {
+ z.Error(err)
+ return err
+ }
+
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+
+ return nil
+}
+
+func (z *ZFSPool) Charts() *module.Charts {
+ return z.charts
+}
+
+func (z *ZFSPool) Collect() map[string]int64 {
+ mx, err := z.collect()
+ if err != nil {
+ z.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (z *ZFSPool) Cleanup() {}
diff --git a/src/go/plugin/go.d/modules/zfspool/zfspool_test.go b/src/go/plugin/go.d/modules/zfspool/zfspool_test.go
new file mode 100644
index 00000000..bf64d171
--- /dev/null
+++ b/src/go/plugin/go.d/modules/zfspool/zfspool_test.go
@@ -0,0 +1,546 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package zfspool
+
+import (
+ "errors"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+
+ dataZpoolList, _ = os.ReadFile("testdata/zpool-list.txt")
+ dataZpoolListWithVdev, _ = os.ReadFile("testdata/zpool-list-vdev.txt")
+ dataZpoolListWithVdevLogsCache, _ = os.ReadFile("testdata/zpool-list-vdev-logs-cache.txt")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+
+ "dataZpoolList": dataZpoolList,
+ "dataZpoolListWithVdev": dataZpoolListWithVdev,
+ "dataZpoolListWithVdevLogsCache": dataZpoolListWithVdevLogsCache,
+ } {
+ require.NotNil(t, data, name)
+
+ }
+}
+
+func TestZFSPool_Configuration(t *testing.T) {
+ module.TestConfigurationSerialize(t, &ZFSPool{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestZFSPool_Init(t *testing.T) {
+ tests := map[string]struct {
+ config Config
+ wantFail bool
+ }{
+ "fails if 'binary_path' is not set": {
+ wantFail: true,
+ config: Config{
+ BinaryPath: "",
+ },
+ },
+ "fails if failed to find binary": {
+ wantFail: true,
+ config: Config{
+ BinaryPath: "zpool!!!",
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ zp := New()
+ zp.Config = test.config
+
+ if test.wantFail {
+ assert.Error(t, zp.Init())
+ } else {
+ assert.NoError(t, zp.Init())
+ }
+ })
+ }
+}
+
+func TestZFSPool_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *ZFSPool
+ }{
+ "not initialized exec": {
+ prepare: func() *ZFSPool {
+ return New()
+ },
+ },
+ "after check": {
+ prepare: func() *ZFSPool {
+ zp := New()
+ zp.exec = prepareMockOk()
+ _ = zp.Check()
+ return zp
+ },
+ },
+ "after collect": {
+ prepare: func() *ZFSPool {
+ zp := New()
+ zp.exec = prepareMockOk()
+ _ = zp.Collect()
+ return zp
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ zp := test.prepare()
+
+ assert.NotPanics(t, zp.Cleanup)
+ })
+ }
+}
+
+func TestZFSPool_Charts(t *testing.T) {
+ assert.NotNil(t, New().Charts())
+}
+
+func TestZFSPool_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockZpoolCLIExec
+ wantFail bool
+ }{
+ "success case": {
+ prepareMock: prepareMockOk,
+ wantFail: false,
+ },
+ "error on list call": {
+ prepareMock: prepareMockErrOnList,
+ wantFail: true,
+ },
+ "empty response": {
+ prepareMock: prepareMockEmptyResponse,
+ wantFail: true,
+ },
+ "unexpected response": {
+ prepareMock: prepareMockUnexpectedResponse,
+ wantFail: true,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ zp := New()
+ mock := test.prepareMock()
+ zp.exec = mock
+
+ if test.wantFail {
+ assert.Error(t, zp.Check())
+ } else {
+ assert.NoError(t, zp.Check())
+ }
+ })
+ }
+}
+
+func TestZFSPool_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockZpoolCLIExec
+ wantMetrics map[string]int64
+ }{
+ "success case": {
+ prepareMock: prepareMockOk,
+ wantMetrics: map[string]int64{
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_degraded": 0,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_faulted": 0,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_offline": 0,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_online": 1,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_removed": 0,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_suspended": 0,
+ "vdev_rpool/mirror-0/nvme0n1p3_health_state_unavail": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_degraded": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_faulted": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_offline": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_online": 1,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_removed": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_suspended": 0,
+ "vdev_rpool/mirror-0/nvme2n1p3_health_state_unavail": 0,
+ "vdev_rpool/mirror-0_health_state_degraded": 0,
+ "vdev_rpool/mirror-0_health_state_faulted": 0,
+ "vdev_rpool/mirror-0_health_state_offline": 0,
+ "vdev_rpool/mirror-0_health_state_online": 1,
+ "vdev_rpool/mirror-0_health_state_removed": 0,
+ "vdev_rpool/mirror-0_health_state_suspended": 0,
+ "vdev_rpool/mirror-0_health_state_unavail": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_degraded": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_faulted": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_offline": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_online": 1,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_removed": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_suspended": 0,
+ "vdev_zion/mirror-0/nvme0n1p3_health_state_unavail": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_degraded": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_faulted": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_offline": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_online": 1,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_removed": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_suspended": 0,
+ "vdev_zion/mirror-0/nvme2n1p3_health_state_unavail": 0,
+ "vdev_zion/mirror-0_health_state_degraded": 0,
+ "vdev_zion/mirror-0_health_state_faulted": 0,
+ "vdev_zion/mirror-0_health_state_offline": 0,
+ "vdev_zion/mirror-0_health_state_online": 1,
+ "vdev_zion/mirror-0_health_state_removed": 0,
+ "vdev_zion/mirror-0_health_state_suspended": 0,
+ "vdev_zion/mirror-0_health_state_unavail": 0,
+ "zpool_rpool_alloc": 9051643576,
+ "zpool_rpool_cap": 42,
+ "zpool_rpool_frag": 33,
+ "zpool_rpool_free": 12240656794,
+ "zpool_rpool_health_state_degraded": 0,
+ "zpool_rpool_health_state_faulted": 0,
+ "zpool_rpool_health_state_offline": 0,
+ "zpool_rpool_health_state_online": 1,
+ "zpool_rpool_health_state_removed": 0,
+ "zpool_rpool_health_state_suspended": 0,
+ "zpool_rpool_health_state_unavail": 0,
+ "zpool_rpool_size": 21367462298,
+ "zpool_zion_health_state_degraded": 0,
+ "zpool_zion_health_state_faulted": 1,
+ "zpool_zion_health_state_offline": 0,
+ "zpool_zion_health_state_online": 0,
+ "zpool_zion_health_state_removed": 0,
+ "zpool_zion_health_state_suspended": 0,
+ "zpool_zion_health_state_unavail": 0,
+ },
+ },
+ "success case vdev logs and cache": {
+ prepareMock: prepareMockOkVdevLogsCache,
+ wantMetrics: map[string]int64{
+ "vdev_rpool/cache/sdb2_health_state_degraded": 0,
+ "vdev_rpool/cache/sdb2_health_state_faulted": 0,
+ "vdev_rpool/cache/sdb2_health_state_offline": 0,
+ "vdev_rpool/cache/sdb2_health_state_online": 1,
+ "vdev_rpool/cache/sdb2_health_state_removed": 0,
+ "vdev_rpool/cache/sdb2_health_state_suspended": 0,
+ "vdev_rpool/cache/sdb2_health_state_unavail": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_degraded": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_faulted": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_offline": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_online": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_removed": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_suspended": 0,
+ "vdev_rpool/cache/wwn-0x500151795954c095-part2_health_state_unavail": 1,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_degraded": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_faulted": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_offline": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_online": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_removed": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_suspended": 0,
+ "vdev_rpool/logs/mirror-1/14807975228228307538_health_state_unavail": 1,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_degraded": 0,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_faulted": 0,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_offline": 0,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_online": 1,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_removed": 0,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_suspended": 0,
+ "vdev_rpool/logs/mirror-1/sdb1_health_state_unavail": 0,
+ "vdev_rpool/logs/mirror-1_health_state_degraded": 1,
+ "vdev_rpool/logs/mirror-1_health_state_faulted": 0,
+ "vdev_rpool/logs/mirror-1_health_state_offline": 0,
+ "vdev_rpool/logs/mirror-1_health_state_online": 0,
+ "vdev_rpool/logs/mirror-1_health_state_removed": 0,
+ "vdev_rpool/logs/mirror-1_health_state_suspended": 0,
+ "vdev_rpool/logs/mirror-1_health_state_unavail": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_degraded": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_faulted": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_offline": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_online": 1,
+ "vdev_rpool/mirror-0/sdc2_health_state_removed": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_suspended": 0,
+ "vdev_rpool/mirror-0/sdc2_health_state_unavail": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_degraded": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_faulted": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_offline": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_online": 1,
+ "vdev_rpool/mirror-0/sdd2_health_state_removed": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_suspended": 0,
+ "vdev_rpool/mirror-0/sdd2_health_state_unavail": 0,
+ "vdev_rpool/mirror-0_health_state_degraded": 0,
+ "vdev_rpool/mirror-0_health_state_faulted": 0,
+ "vdev_rpool/mirror-0_health_state_offline": 0,
+ "vdev_rpool/mirror-0_health_state_online": 1,
+ "vdev_rpool/mirror-0_health_state_removed": 0,
+ "vdev_rpool/mirror-0_health_state_suspended": 0,
+ "vdev_rpool/mirror-0_health_state_unavail": 0,
+ "vdev_zion/cache/sdb2_health_state_degraded": 0,
+ "vdev_zion/cache/sdb2_health_state_faulted": 0,
+ "vdev_zion/cache/sdb2_health_state_offline": 0,
+ "vdev_zion/cache/sdb2_health_state_online": 1,
+ "vdev_zion/cache/sdb2_health_state_removed": 0,
+ "vdev_zion/cache/sdb2_health_state_suspended": 0,
+ "vdev_zion/cache/sdb2_health_state_unavail": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_degraded": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_faulted": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_offline": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_online": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_removed": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_suspended": 0,
+ "vdev_zion/cache/wwn-0x500151795954c095-part2_health_state_unavail": 1,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_degraded": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_faulted": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_offline": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_online": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_removed": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_suspended": 0,
+ "vdev_zion/logs/mirror-1/14807975228228307538_health_state_unavail": 1,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_degraded": 0,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_faulted": 0,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_offline": 0,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_online": 1,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_removed": 0,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_suspended": 0,
+ "vdev_zion/logs/mirror-1/sdb1_health_state_unavail": 0,
+ "vdev_zion/logs/mirror-1_health_state_degraded": 1,
+ "vdev_zion/logs/mirror-1_health_state_faulted": 0,
+ "vdev_zion/logs/mirror-1_health_state_offline": 0,
+ "vdev_zion/logs/mirror-1_health_state_online": 0,
+ "vdev_zion/logs/mirror-1_health_state_removed": 0,
+ "vdev_zion/logs/mirror-1_health_state_suspended": 0,
+ "vdev_zion/logs/mirror-1_health_state_unavail": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_degraded": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_faulted": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_offline": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_online": 1,
+ "vdev_zion/mirror-0/sdc2_health_state_removed": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_suspended": 0,
+ "vdev_zion/mirror-0/sdc2_health_state_unavail": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_degraded": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_faulted": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_offline": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_online": 1,
+ "vdev_zion/mirror-0/sdd2_health_state_removed": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_suspended": 0,
+ "vdev_zion/mirror-0/sdd2_health_state_unavail": 0,
+ "vdev_zion/mirror-0_health_state_degraded": 0,
+ "vdev_zion/mirror-0_health_state_faulted": 0,
+ "vdev_zion/mirror-0_health_state_offline": 0,
+ "vdev_zion/mirror-0_health_state_online": 1,
+ "vdev_zion/mirror-0_health_state_removed": 0,
+ "vdev_zion/mirror-0_health_state_suspended": 0,
+ "vdev_zion/mirror-0_health_state_unavail": 0,
+ "zpool_rpool_alloc": 9051643576,
+ "zpool_rpool_cap": 42,
+ "zpool_rpool_frag": 33,
+ "zpool_rpool_free": 12240656794,
+ "zpool_rpool_health_state_degraded": 0,
+ "zpool_rpool_health_state_faulted": 0,
+ "zpool_rpool_health_state_offline": 0,
+ "zpool_rpool_health_state_online": 1,
+ "zpool_rpool_health_state_removed": 0,
+ "zpool_rpool_health_state_suspended": 0,
+ "zpool_rpool_health_state_unavail": 0,
+ "zpool_rpool_size": 21367462298,
+ "zpool_zion_health_state_degraded": 0,
+ "zpool_zion_health_state_faulted": 1,
+ "zpool_zion_health_state_offline": 0,
+ "zpool_zion_health_state_online": 0,
+ "zpool_zion_health_state_removed": 0,
+ "zpool_zion_health_state_suspended": 0,
+ "zpool_zion_health_state_unavail": 0,
+ },
+ },
+ "error on list call": {
+ prepareMock: prepareMockErrOnList,
+ wantMetrics: nil,
+ },
+ "empty response": {
+ prepareMock: prepareMockEmptyResponse,
+ wantMetrics: nil,
+ },
+ "unexpected response": {
+ prepareMock: prepareMockUnexpectedResponse,
+ wantMetrics: nil,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ zp := New()
+ mock := test.prepareMock()
+ zp.exec = mock
+
+ mx := zp.Collect()
+
+ assert.Equal(t, test.wantMetrics, mx)
+
+ if len(test.wantMetrics) > 0 {
+ want := len(zpoolChartsTmpl)*len(zp.seenZpools) + len(vdevChartsTmpl)*len(zp.seenVdevs)
+
+ assert.Len(t, *zp.Charts(), want, "want charts")
+
+ module.TestMetricsHasAllChartsDimsSkip(t, zp.Charts(), mx, func(chart *module.Chart) bool {
+ return strings.HasPrefix(chart.ID, "zfspool_zion") && !strings.HasSuffix(chart.ID, "health_state")
+ })
+ }
+ })
+ }
+}
+
+func TestZFSPool_parseZpoolListDevOutput(t *testing.T) {
+ tests := map[string]struct {
+ input string
+ want []vdevEntry
+ }{
+ "": {
+ input: `
+NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
+store 9981503995904 3046188658688 6935315337216 - - 9 30 1.00 DEGRADED -
+ mirror-0 9981503995904 3046188658688 6935315337216 - - 9 30 - ONLINE
+ sdc2 9998683602944 - - - - - - - ONLINE
+ sdd2 9998683602944 - - - - - - - ONLINE
+logs - - - - - - - - -
+ mirror-1 17716740096 393216 17716346880 - - 0 0 - DEGRADED
+ sdb1 17951621120 - - - - - - - ONLINE
+ 14807975228228307538 - - - - - - - - UNAVAIL
+cache - - - - - - - - -
+ sdb2 99000254464 98755866624 239665152 - - 0 99 - ONLINE
+ wwn-0x500151795954c095-part2 - - - - - - - - UNAVAIL
+`,
+ want: []vdevEntry{
+ {
+ name: "mirror-0",
+ health: "online",
+ vdev: "store/mirror-0",
+ level: 2,
+ },
+ {
+ name: "sdc2",
+ health: "online",
+ vdev: "store/mirror-0/sdc2",
+ level: 4,
+ },
+ {
+ name: "sdd2",
+ health: "online",
+ vdev: "store/mirror-0/sdd2",
+ level: 4,
+ },
+ {
+ name: "logs",
+ health: "-",
+ vdev: "store/logs",
+ level: 0,
+ },
+ {
+ name: "mirror-1",
+ health: "degraded",
+ vdev: "store/logs/mirror-1",
+ level: 2,
+ },
+ {
+ name: "sdb1",
+ health: "online",
+ vdev: "store/logs/mirror-1/sdb1",
+ level: 4,
+ },
+ {
+ name: "14807975228228307538",
+ health: "unavail",
+ vdev: "store/logs/mirror-1/14807975228228307538",
+ level: 4,
+ },
+ {
+ name: "cache",
+ health: "-",
+ vdev: "store/cache",
+ level: 0,
+ },
+ {
+ name: "sdb2",
+ health: "online",
+ vdev: "store/cache/sdb2",
+ level: 2,
+ },
+ {
+ name: "wwn-0x500151795954c095-part2",
+ health: "unavail",
+ vdev: "store/cache/wwn-0x500151795954c095-part2",
+ level: 2,
+ },
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ v, err := parseZpoolListVdevOutput([]byte(test.input))
+ require.NoError(t, err)
+ assert.Equal(t, test.want, v)
+ })
+ }
+}
+
+func prepareMockOk() *mockZpoolCLIExec {
+ return &mockZpoolCLIExec{
+ listData: dataZpoolList,
+ listWithVdevData: dataZpoolListWithVdev,
+ }
+}
+
+func prepareMockOkVdevLogsCache() *mockZpoolCLIExec {
+ return &mockZpoolCLIExec{
+ listData: dataZpoolList,
+ listWithVdevData: dataZpoolListWithVdevLogsCache,
+ }
+}
+
+func prepareMockErrOnList() *mockZpoolCLIExec {
+ return &mockZpoolCLIExec{
+ errOnList: true,
+ }
+}
+
+func prepareMockEmptyResponse() *mockZpoolCLIExec {
+ return &mockZpoolCLIExec{}
+}
+
+func prepareMockUnexpectedResponse() *mockZpoolCLIExec {
+ return &mockZpoolCLIExec{
+ listData: []byte(`
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
+`),
+ }
+}
+
+type mockZpoolCLIExec struct {
+ errOnList bool
+ listData []byte
+ listWithVdevData []byte
+}
+
+func (m *mockZpoolCLIExec) list() ([]byte, error) {
+ if m.errOnList {
+ return nil, errors.New("mock.list() error")
+ }
+
+ return m.listData, nil
+}
+
+func (m *mockZpoolCLIExec) listWithVdev(pool string) ([]byte, error) {
+ s := string(m.listWithVdevData)
+ s = strings.Replace(s, "rpool", pool, 1)
+
+ return []byte(s), nil
+}