package main import ( "context" "os/exec" "strconv" "strings" "sync" "time" ) type ZFSPoolMetrics struct { Name string `json:"name"` Health string `json:"health"` SizeBytes uint64 `json:"sizeBytes"` AllocatedBytes uint64 `json:"allocatedBytes"` FreeBytes uint64 `json:"freeBytes"` CapacityPercent float64 `json:"capacityPercent"` Fragmentation float64 `json:"fragmentationPercent"` DedupRatio string `json:"dedupRatio"` Scan string `json:"scan"` Errors string `json:"errors"` AttentionReasons []string `json:"attentionReasons"` } type ZFSMetrics struct { Available bool `json:"available"` Pools []ZFSPoolMetrics `json:"pools"` } type zfsCollector struct { mu sync.Mutex cached ZFSMetrics updatedAt time.Time } func (c *zfsCollector) collect() ZFSMetrics { c.mu.Lock() defer c.mu.Unlock() if time.Since(c.updatedAt) < 30*time.Second { return c.cached } c.cached = readZFSPools() c.updatedAt = time.Now() return c.cached } func readZFSPools() ZFSMetrics { if _, err := exec.LookPath("zpool"); err != nil { return ZFSMetrics{} } ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() output, err := exec.CommandContext(ctx, "zpool", "list", "-H", "-p", "-o", "name,size,allocated,free,fragmentation,capacity,dedupratio,health").Output() result := ZFSMetrics{Available: true} if err != nil { return result } for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") { fields := strings.Fields(line) if len(fields) < 8 { continue } pool := ZFSPoolMetrics{Name: fields[0], DedupRatio: fields[6], Health: fields[7]} pool.SizeBytes, _ = strconv.ParseUint(fields[1], 10, 64) pool.AllocatedBytes, _ = strconv.ParseUint(fields[2], 10, 64) pool.FreeBytes, _ = strconv.ParseUint(fields[3], 10, 64) pool.Fragmentation = parsePercent(fields[4]) pool.CapacityPercent = parsePercent(fields[5]) pool.Scan, pool.Errors = readZFSStatus(pool.Name) if pool.Health != "ONLINE" { pool.AttentionReasons = append(pool.AttentionReasons, "Состояние пула: "+pool.Health) } if pool.Errors != "" && !strings.Contains(pool.Errors, "No known data errors") { pool.AttentionReasons = append(pool.AttentionReasons, pool.Errors) } result.Pools = append(result.Pools, pool) } return result } func parsePercent(value string) float64 { parsed, _ := strconv.ParseFloat(strings.TrimSuffix(value, "%"), 64) return parsed } func readZFSStatus(pool string) (string, string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() output, _ := exec.CommandContext(ctx, "zpool", "status", pool).Output() var scan, errors string for _, line := range strings.Split(string(output), "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "scan:") { scan = strings.TrimSpace(strings.TrimPrefix(trimmed, "scan:")) } if strings.HasPrefix(trimmed, "errors:") { errors = strings.TrimSpace(strings.TrimPrefix(trimmed, "errors:")) } } return scan, errors }