103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
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
|
|
}
|