From 91679b5cc024650d579dff4130c7513c7f83cbc5 Mon Sep 17 00:00:00 2001 From: henrygd Date: Fri, 25 Jul 2025 18:07:11 -0400 Subject: [PATCH] refactor load average handling (#982) - Transitioned from individual load average fields (LoadAvg1, LoadAvg5, LoadAvg15) to a single array (LoadAvg) - Ensure load is displayed when all zero values. --- beszel/internal/agent/system.go | 17 ++++--- beszel/internal/alerts/alerts.go | 4 +- beszel/internal/alerts/alerts_system.go | 12 ++--- beszel/internal/entities/system/system.go | 48 ++++++++++--------- beszel/internal/records/records.go | 12 ++--- .../components/charts/load-average-chart.tsx | 35 ++++++++------ beszel/site/src/components/routes/system.tsx | 4 +- .../systems-table/systems-table.tsx | 29 +++++++---- beszel/site/src/lib/utils.ts | 12 +++++ beszel/site/src/types.d.ts | 12 +++++ 10 files changed, 116 insertions(+), 69 deletions(-) diff --git a/beszel/internal/agent/system.go b/beszel/internal/agent/system.go index 4a9c2c6..215b504 100644 --- a/beszel/internal/agent/system.go +++ b/beszel/internal/agent/system.go @@ -80,10 +80,11 @@ func (a *Agent) getSystemStats() system.Stats { // load average if avgstat, err := load.Avg(); err == nil { - systemStats.LoadAvg1 = twoDecimals(avgstat.Load1) - systemStats.LoadAvg5 = twoDecimals(avgstat.Load5) - systemStats.LoadAvg15 = twoDecimals(avgstat.Load15) - slog.Debug("Load average", "5m", systemStats.LoadAvg5, "15m", systemStats.LoadAvg15) + // TODO: remove these in future release in favor of load avg array + systemStats.LoadAvg[0] = avgstat.Load1 + systemStats.LoadAvg[1] = avgstat.Load5 + systemStats.LoadAvg[2] = avgstat.Load15 + slog.Debug("Load average", "5m", avgstat.Load5, "15m", avgstat.Load15) } else { slog.Error("Error getting load average", "err", err) } @@ -255,9 +256,11 @@ func (a *Agent) getSystemStats() system.Stats { // update base system info a.systemInfo.Cpu = systemStats.Cpu - a.systemInfo.LoadAvg1 = systemStats.LoadAvg1 - a.systemInfo.LoadAvg5 = systemStats.LoadAvg5 - a.systemInfo.LoadAvg15 = systemStats.LoadAvg15 + a.systemInfo.LoadAvg = systemStats.LoadAvg + // TODO: remove these in future release in favor of load avg array + a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0] + a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1] + a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2] a.systemInfo.MemPct = systemStats.MemPct a.systemInfo.DiskPct = systemStats.DiskPct a.systemInfo.Uptime, _ = host.Uptime() diff --git a/beszel/internal/alerts/alerts.go b/beszel/internal/alerts/alerts.go index b6a0533..e796a46 100644 --- a/beszel/internal/alerts/alerts.go +++ b/beszel/internal/alerts/alerts.go @@ -47,9 +47,7 @@ type SystemAlertStats struct { NetSent float64 `json:"ns"` NetRecv float64 `json:"nr"` Temperatures map[string]float32 `json:"t"` - LoadAvg1 float64 `json:"l1"` - LoadAvg5 float64 `json:"l5"` - LoadAvg15 float64 `json:"l15"` + LoadAvg [3]float64 `json:"la"` } type SystemAlertData struct { diff --git a/beszel/internal/alerts/alerts_system.go b/beszel/internal/alerts/alerts_system.go index 7c99a95..c261c69 100644 --- a/beszel/internal/alerts/alerts_system.go +++ b/beszel/internal/alerts/alerts_system.go @@ -55,13 +55,13 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst val = data.Info.DashboardTemp unit = "°C" case "LoadAvg1": - val = data.Info.LoadAvg1 + val = data.Info.LoadAvg[0] unit = "" case "LoadAvg5": - val = data.Info.LoadAvg5 + val = data.Info.LoadAvg[1] unit = "" case "LoadAvg15": - val = data.Info.LoadAvg15 + val = data.Info.LoadAvg[2] unit = "" } @@ -200,11 +200,11 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst alert.mapSums[key] += temp } case "LoadAvg1": - alert.val += stats.LoadAvg1 + alert.val += stats.LoadAvg[0] case "LoadAvg5": - alert.val += stats.LoadAvg5 + alert.val += stats.LoadAvg[1] case "LoadAvg15": - alert.val += stats.LoadAvg15 + alert.val += stats.LoadAvg[2] default: continue } diff --git a/beszel/internal/entities/system/system.go b/beszel/internal/entities/system/system.go index 7be298f..9efde5c 100644 --- a/beszel/internal/entities/system/system.go +++ b/beszel/internal/entities/system/system.go @@ -31,11 +31,13 @@ type Stats struct { Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"` ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"` GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"` - LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"` - LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"` - LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"` + LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"` + LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"` + LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"` Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes] MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes] + LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"` + // TODO: remove other load fields in future release in favor of load avg array } type GPUData struct { @@ -79,25 +81,27 @@ const ( ) type Info struct { - Hostname string `json:"h" cbor:"0,keyasint"` - KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` - Cores int `json:"c" cbor:"2,keyasint"` - Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"` - CpuModel string `json:"m" cbor:"4,keyasint"` - Uptime uint64 `json:"u" cbor:"5,keyasint"` - Cpu float64 `json:"cpu" cbor:"6,keyasint"` - MemPct float64 `json:"mp" cbor:"7,keyasint"` - DiskPct float64 `json:"dp" cbor:"8,keyasint"` - Bandwidth float64 `json:"b" cbor:"9,keyasint"` - AgentVersion string `json:"v" cbor:"10,keyasint"` - Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` - GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"` - DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"` - Os Os `json:"os" cbor:"14,keyasint"` - LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` - LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` - LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` - BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` + Hostname string `json:"h" cbor:"0,keyasint"` + KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` + Cores int `json:"c" cbor:"2,keyasint"` + Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"` + CpuModel string `json:"m" cbor:"4,keyasint"` + Uptime uint64 `json:"u" cbor:"5,keyasint"` + Cpu float64 `json:"cpu" cbor:"6,keyasint"` + MemPct float64 `json:"mp" cbor:"7,keyasint"` + DiskPct float64 `json:"dp" cbor:"8,keyasint"` + Bandwidth float64 `json:"b" cbor:"9,keyasint"` + AgentVersion string `json:"v" cbor:"10,keyasint"` + Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` + GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"` + DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"` + Os Os `json:"os" cbor:"14,keyasint"` + LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` + LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` + LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` + BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` + LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` + // TODO: remove load fields in future release in favor of load avg array } // Final data structure to return to the hub diff --git a/beszel/internal/records/records.go b/beszel/internal/records/records.go index a1856ee..7dd782b 100644 --- a/beszel/internal/records/records.go +++ b/beszel/internal/records/records.go @@ -203,9 +203,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.DiskWritePs += stats.DiskWritePs sum.NetworkSent += stats.NetworkSent sum.NetworkRecv += stats.NetworkRecv - sum.LoadAvg1 += stats.LoadAvg1 - sum.LoadAvg5 += stats.LoadAvg5 - sum.LoadAvg15 += stats.LoadAvg15 + sum.LoadAvg[0] += stats.LoadAvg[0] + sum.LoadAvg[1] += stats.LoadAvg[1] + sum.LoadAvg[2] += stats.LoadAvg[2] sum.Bandwidth[0] += stats.Bandwidth[0] sum.Bandwidth[1] += stats.Bandwidth[1] // Set peak values @@ -285,9 +285,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count) sum.NetworkSent = twoDecimals(sum.NetworkSent / count) sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count) - sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count) - sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count) - sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count) + sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count) + sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count) + sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count) sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count) sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count) // Average temperatures diff --git a/beszel/site/src/components/charts/load-average-chart.tsx b/beszel/site/src/components/charts/load-average-chart.tsx index 752993a..f5d4c5b 100644 --- a/beszel/site/src/components/charts/load-average-chart.tsx +++ b/beszel/site/src/components/charts/load-average-chart.tsx @@ -9,31 +9,30 @@ import { xAxis, } from "@/components/ui/chart" import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils" -import { ChartData } from "@/types" +import { ChartData, SystemStats } from "@/types" import { memo } from "react" import { t } from "@lingui/core/macro" export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - if (chartData.systemStats.length === 0) { - return null - } - - const keys = { - l1: { + const keys: { legacy: keyof SystemStats; color: string; label: string }[] = [ + { + legacy: "l1", color: "hsl(271, 81%, 60%)", // Purple label: t({ message: `1 min`, comment: "Load average" }), }, - l5: { + { + legacy: "l5", color: "hsl(217, 91%, 60%)", // Blue label: t({ message: `5 min`, comment: "Load average" }), }, - l15: { + { + legacy: "l15", color: "hsl(25, 95%, 53%)", // Orange label: t({ message: `15 min`, comment: "Load average" }), }, - } + ] return (
@@ -69,16 +68,22 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD /> } /> - {Object.entries(keys).map(([key, value]: [string, { color: string; label: string }]) => { + {keys.map(({ legacy, color, label }, i) => { + const dataKey = (value: { stats: SystemStats }) => { + if (chartData.agentVersion.patch < 1) { + return value.stats?.[legacy] + } + return value.stats?.la?.[i] ?? value.stats?.[legacy] + } return ( ) diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx index 8b51634..4f7c3dc 100644 --- a/beszel/site/src/components/routes/system.tsx +++ b/beszel/site/src/components/routes/system.tsx @@ -26,6 +26,7 @@ import { getHostDisplayValue, getPbTimestamp, listen, + parseSemVer, toFixedFloat, useLocalStorage, } from "@/lib/utils" @@ -191,6 +192,7 @@ export default function SystemDetail({ name }: { name: string }) { chartTime, orientation: direction === "rtl" ? "right" : "left", ...getTimeData(chartTime, lastCreated), + agentVersion: parseSemVer(system?.info?.v), } }, [systemStats, containerData, direction]) @@ -642,7 +644,7 @@ export default function SystemDetail({ name }: { name: string }) { )} {/* Load Average chart */} - {system.info.l1 !== undefined && ( + {chartData.agentVersion?.minor >= 12 && ( { - const { l1 = 0, l5 = 0, l15 = 0 } = info - return l1 + l5 + l15 + const sum = info.la?.reduce((acc, curr) => acc + curr, 0) + // TODO: remove this in future release in favor of la array + if (!sum) { + return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) + } + return sum }, name: () => t({ message: "Load Avg", comment: "Short label for load average" }), size: 0, @@ -233,16 +238,22 @@ export default function SystemsTable() { header: sortableHeader, cell(info: CellContext) { const { info: sysInfo, status } = info.row.original - if (sysInfo.l1 === undefined) { + // agent version + const { minor, patch } = parseSemVer(sysInfo.v) + let loadAverages = sysInfo.la + + // use legacy load averages if agent version is less than 12.1.0 + if (!loadAverages || (minor === 12 && patch < 1)) { + loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0] + } + + const max = Math.max(...loadAverages) + if (max === 0 && (status === "paused" || minor < 12)) { return null } - const { l1 = 0, l5 = 0, l15 = 0, t: cpuThreads = 1 } = sysInfo - const loadAverages = [l1, l5, l15] - function getDotColor() { - const max = Math.max(...loadAverages) - const normalized = max / cpuThreads + const normalized = max / (sysInfo.t ?? 1) if (status !== "up") return "bg-primary/30" if (normalized < 0.7) return "bg-green-500" if (normalized < 1) return "bg-yellow-500" @@ -252,7 +263,7 @@ export default function SystemsTable() { return (
- {loadAverages.map((la, i) => ( + {loadAverages?.map((la, i) => ( {decimalString(la, la >= 10 ? 1 : 2)} ))}
diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts index 25ee51a..5fc0453 100644 --- a/beszel/site/src/lib/utils.ts +++ b/beszel/site/src/lib/utils.ts @@ -9,6 +9,7 @@ import { ChartTimeData, ChartTimes, FingerprintRecord, + SemVer, SystemRecord, UserSettings, } from "@/types" @@ -495,3 +496,14 @@ export function formatDuration( .filter(Boolean) .join(" ") } + +export const parseSemVer = (semVer = ""): SemVer => { + // if (semVer.startsWith("v")) { + // semVer = semVer.slice(1) + // } + if (semVer.includes("-")) { + semVer = semVer.slice(0, semVer.indexOf("-")) + } + const parts = semVer.split(".").map(Number) + return { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 } +} diff --git a/beszel/site/src/types.d.ts b/beszel/site/src/types.d.ts index 3cf036d..3cf3a6d 100644 --- a/beszel/site/src/types.d.ts +++ b/beszel/site/src/types.d.ts @@ -50,6 +50,8 @@ export interface SystemInfo { l5?: number /** load average 15 minutes */ l15?: number + /** load average */ + la?: [number, number, number] /** operating system */ o?: string /** uptime */ @@ -79,12 +81,15 @@ export interface SystemStats { cpu: number /** peak cpu */ cpum?: number + // TODO: remove these in future release in favor of la /** load average 1 minute */ l1?: number /** load average 5 minutes */ l5?: number /** load average 15 minutes */ l15?: number + /** load average */ + la?: [number, number, number] /** total memory (gb) */ m: number /** memory used (gb) */ @@ -234,7 +239,14 @@ type ChartDataContainer = { [key: string]: key extends "created" ? never : ContainerStats } +export interface SemVer { + major: number + minor: number + patch: number +} + export interface ChartData { + agentVersion: SemVer systemStats: SystemStatsRecord[] containerData: ChartDataContainer[] orientation: "right" | "left"