mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 08:57:48 +02:00
feat: add ZFS monitoring (#2209)
- track pool capacity, health, I/O, scrub status, and vdev errors - report dataset usage and correct ZFS filesystem metrics - add pool charts, detail views, refresh controls, and health alerts - persist pool details and include ZFS usage in disk alerts - support configurable detail intervals and legacy agent compatibility --------- Co-authored-by: hank <hank@henrygd.me>
This commit is contained in:
160
agent/zfs/zfs.go
Normal file
160
agent/zfs/zfs.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Package zfs provides functions to read ZFS statistics.
|
||||
package zfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var commandTimeout = 10 * time.Second
|
||||
|
||||
var commandOutput = func(name string, args ...string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), commandTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C")
|
||||
out, err := cmd.Output()
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("%s timed out after %s: %w", name, commandTimeout, ctx.Err())
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ErrNoZfs is returned when the ZFS utilities or kernel interfaces are unavailable.
|
||||
var ErrNoZfs = errors.New("zfs utilities unavailable")
|
||||
|
||||
// PoolStat is a snapshot of a ZFS pool's capacity and health.
|
||||
type PoolStat struct {
|
||||
Name string
|
||||
Size uint64 // total capacity in bytes
|
||||
Alloc uint64 // allocated bytes
|
||||
Free uint64 // free bytes
|
||||
Health string // ONLINE, DEGRADED, FAULTED, ...
|
||||
}
|
||||
|
||||
// PoolKernelStat is the inexpensive pool telemetry exposed by the ZFS kernel.
|
||||
// NRead and NWrite are cumulative byte counters since the pool was imported.
|
||||
type PoolKernelStat struct {
|
||||
Name string
|
||||
Health string
|
||||
NRead uint64
|
||||
NWrite uint64
|
||||
}
|
||||
|
||||
// PoolIoStats holds calculated per-second I/O rates for a pool.
|
||||
type PoolIoStats struct {
|
||||
NRead uint64
|
||||
NWrite uint64
|
||||
}
|
||||
|
||||
// Dataset is a single ZFS dataset with usage information.
|
||||
type Dataset struct {
|
||||
Name string
|
||||
Used uint64
|
||||
Avail uint64
|
||||
Mountpoint string
|
||||
}
|
||||
|
||||
// PoolStats returns capacity and health for all pools on the system using
|
||||
// `zpool list`. Frequent health and I/O sampling uses PoolKernelStats instead.
|
||||
func PoolStats() ([]PoolStat, error) {
|
||||
out, err := commandOutput("zpool", "list", "-Hp", "-o", "name,size,alloc,free,health")
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) && strings.Contains(string(exitErr.Stderr), "no pools available") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("zpool list: %w", err)
|
||||
}
|
||||
return parseZpoolListOutput(out)
|
||||
}
|
||||
|
||||
// Datasets returns all datasets on the system with usage and mountpoint
|
||||
// information using `zfs list` (recursive by default).
|
||||
func Datasets() ([]Dataset, error) {
|
||||
out, err := commandOutput("zfs", "list", "-Hp", "-o", "name,used,avail,mountpoint")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zfs list: %w", err)
|
||||
}
|
||||
return parseZfsListOutput(out)
|
||||
}
|
||||
|
||||
// parseZpoolListOutput parses `zpool list -Hp -o name,size,alloc,free,health` output.
|
||||
// Columns are tab-separated; numeric columns are raw bytes.
|
||||
func parseZpoolListOutput(out []byte) ([]PoolStat, error) {
|
||||
var pools []PoolStat
|
||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if line == "no pools available" && len(pools) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
fields := strings.Split(line, "\t")
|
||||
if len(fields) < 5 {
|
||||
return nil, fmt.Errorf("unexpected zpool list line: %q", line)
|
||||
}
|
||||
size, err := strconv.ParseUint(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing size for pool %q: %w", fields[0], err)
|
||||
}
|
||||
alloc, err := strconv.ParseUint(fields[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing alloc for pool %q: %w", fields[0], err)
|
||||
}
|
||||
free, err := strconv.ParseUint(fields[3], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing free for pool %q: %w", fields[0], err)
|
||||
}
|
||||
pools = append(pools, PoolStat{
|
||||
Name: fields[0],
|
||||
Size: size,
|
||||
Alloc: alloc,
|
||||
Free: free,
|
||||
Health: fields[4],
|
||||
})
|
||||
}
|
||||
return pools, scanner.Err()
|
||||
}
|
||||
|
||||
// parseZfsListOutput parses `zfs list -Hp -o name,used,avail,mountpoint` output.
|
||||
// The mountpoint column may contain spaces, so it is split on tabs only.
|
||||
func parseZfsListOutput(out []byte) ([]Dataset, error) {
|
||||
var datasets []Dataset
|
||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.SplitN(line, "\t", 4)
|
||||
if len(fields) < 4 {
|
||||
return nil, fmt.Errorf("unexpected zfs list line: %q", line)
|
||||
}
|
||||
used, err := strconv.ParseUint(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing used for dataset %q: %w", fields[0], err)
|
||||
}
|
||||
avail, err := strconv.ParseUint(fields[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing avail for dataset %q: %w", fields[0], err)
|
||||
}
|
||||
datasets = append(datasets, Dataset{
|
||||
Name: fields[0],
|
||||
Used: used,
|
||||
Avail: avail,
|
||||
Mountpoint: fields[3],
|
||||
})
|
||||
}
|
||||
return datasets, scanner.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user