diff --git a/beszel/internal/agent/agent.go b/beszel/internal/agent/agent.go index 4244fcc..332f8c9 100644 --- a/beszel/internal/agent/agent.go +++ b/beszel/internal/agent/agent.go @@ -13,6 +13,7 @@ import ( type Agent struct { debug bool // true if LOG_LEVEL is set to debug + zfs bool // true if system has arcstats memCalc string // Memory calculation formula fsNames []string // List of filesystem device names being monitored fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem diff --git a/beszel/internal/agent/system.go b/beszel/internal/agent/system.go index 7cee4d3..aa4d9c2 100644 --- a/beszel/internal/agent/system.go +++ b/beszel/internal/agent/system.go @@ -3,9 +3,12 @@ package agent import ( "beszel" "beszel/internal/entities/system" + "bufio" + "fmt" "log/slog" "os" "strconv" + "strings" "time" "github.com/shirou/gopsutil/v4/cpu" @@ -36,6 +39,13 @@ func (a *Agent) initializeSystemInfo() { a.systemInfo.Threads = threads } } + + // zfs + if _, err := getARCSize(); err == nil { + a.zfs = true + } else { + slog.Debug("Not monitoring ZFS ARC", "err", err) + } } // Returns current info, stats about the host system @@ -52,6 +62,9 @@ func (a *Agent) getSystemStats() system.Stats { // memory if v, err := mem.VirtualMemory(); err == nil { + // swap + systemStats.Swap = bytesToGigabytes(v.SwapTotal) + systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached) // cache + buffers value for default mem calculation cacheBuff := v.Total - v.Free - v.Used // htop memory calculation overrides @@ -61,9 +74,15 @@ func (a *Agent) getSystemStats() system.Stats { v.Used = v.Total - (v.Free + cacheBuff) v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0 } + // subtract ZFS ARC size from used memory and add as its own category + if a.zfs { + if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used { + v.Used = v.Used - arcSize + v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0 + systemStats.MemZfsArc = bytesToGigabytes(arcSize) + } + } systemStats.Mem = bytesToGigabytes(v.Total) - systemStats.Swap = bytesToGigabytes(v.SwapTotal) - systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached) systemStats.MemBuffCache = bytesToGigabytes(cacheBuff) systemStats.MemUsed = bytesToGigabytes(v.Used) systemStats.MemPct = twoDecimals(v.UsedPercent) @@ -192,3 +211,29 @@ func (a *Agent) getSystemStats() system.Stats { return systemStats } + +// Returns the size of the ZFS ARC memory cache in bytes +func getARCSize() (uint64, error) { + file, err := os.Open("/proc/spl/kstat/zfs/arcstats") + if err != nil { + return 0, err + } + defer file.Close() + + // Scan the lines + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "size") { + // Example line: size 4 15032385536 + fields := strings.Fields(line) + if len(fields) < 3 { + return 0, err + } + // Return the size as uint64 + return strconv.ParseUint(fields[2], 10, 64) + } + } + + return 0, fmt.Errorf("failed to parse size field") +} diff --git a/beszel/internal/entities/system/system.go b/beszel/internal/entities/system/system.go index 6ffa39e..d3f2626 100644 --- a/beszel/internal/entities/system/system.go +++ b/beszel/internal/entities/system/system.go @@ -11,6 +11,7 @@ type Stats struct { MemUsed float64 `json:"mu"` MemPct float64 `json:"mp"` MemBuffCache float64 `json:"mb"` + MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory Swap float64 `json:"s,omitempty"` SwapUsed float64 `json:"su,omitempty"` DiskTotal float64 `json:"d"` diff --git a/beszel/site/src/components/charts/mem-chart.tsx b/beszel/site/src/components/charts/mem-chart.tsx index 840bbe9..f6d0fe3 100644 --- a/beszel/site/src/components/charts/mem-chart.tsx +++ b/beszel/site/src/components/charts/mem-chart.tsx @@ -1,16 +1,8 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' -import { - useYAxisWidth, - chartTimeData, - cn, - formatShortDate, - toFixedFloat, - twoDecimalString, -} from '@/lib/utils' +import { useYAxisWidth, chartTimeData, cn, toFixedFloat, twoDecimalString } from '@/lib/utils' import { useMemo } from 'react' -// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' @@ -79,16 +71,16 @@ export default function MemChart({ content={ a.name.localeCompare(b.name)} - labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} + itemSorter={(a, b) => a.order - b.order} contentFormatter={(item) => twoDecimalString(item.value) + ' GB'} indicator="line" /> } /> + {systemData.at(-1)?.stats.mz && ( + + )}