diff options
Diffstat (limited to 'src/go/plugin/go.d/modules/zfspool')
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 +} |