diff --git a/beszel/internal/agent/system.go b/beszel/internal/agent/system.go index b3ec974..4a9c2c6 100644 --- a/beszel/internal/agent/system.go +++ b/beszel/internal/agent/system.go @@ -174,24 +174,27 @@ func (a *Agent) getSystemStats() system.Stats { a.initializeNetIoStats() } if netIO, err := psutilNet.IOCounters(true); err == nil { - secondsElapsed := time.Since(a.netIoStats.Time).Seconds() + msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds()) a.netIoStats.Time = time.Now() - bytesSent := uint64(0) - bytesRecv := uint64(0) + totalBytesSent := uint64(0) + totalBytesRecv := uint64(0) // sum all bytes sent and received for _, v := range netIO { // skip if not in valid network interfaces list if _, exists := a.netInterfaces[v.Name]; !exists { continue } - bytesSent += v.BytesSent - bytesRecv += v.BytesRecv + totalBytesSent += v.BytesSent + totalBytesRecv += v.BytesRecv } // add to systemStats - sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed - recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed - networkSentPs := bytesToMegabytes(sentPerSecond) - networkRecvPs := bytesToMegabytes(recvPerSecond) + var bytesSentPerSecond, bytesRecvPerSecond uint64 + if msElapsed > 0 { + bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed + bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed + } + networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond)) + networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond)) // add check for issue (#150) where sent is a massive number if networkSentPs > 10_000 || networkRecvPs > 10_000 { slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs) @@ -206,9 +209,10 @@ func (a *Agent) getSystemStats() system.Stats { } else { systemStats.NetworkSent = networkSentPs systemStats.NetworkRecv = networkRecvPs + systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond // update netIoStats - a.netIoStats.BytesSent = bytesSent - a.netIoStats.BytesRecv = bytesRecv + a.netIoStats.BytesSent = totalBytesSent + a.netIoStats.BytesRecv = totalBytesRecv } } @@ -257,7 +261,9 @@ func (a *Agent) getSystemStats() system.Stats { a.systemInfo.MemPct = systemStats.MemPct a.systemInfo.DiskPct = systemStats.DiskPct a.systemInfo.Uptime, _ = host.Uptime() + // TODO: in future release, remove MB bandwidth values in favor of bytes a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv) + a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1] slog.Debug("sysinfo", "data", a.systemInfo) return systemStats diff --git a/beszel/internal/entities/system/system.go b/beszel/internal/entities/system/system.go index 15bc1c1..7be298f 100644 --- a/beszel/internal/entities/system/system.go +++ b/beszel/internal/entities/system/system.go @@ -34,6 +34,8 @@ type Stats struct { 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"` + 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] } type GPUData struct { @@ -77,24 +79,25 @@ 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"` + 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"` } // Final data structure to return to the hub diff --git a/beszel/internal/records/records.go b/beszel/internal/records/records.go index cfd2e42..a1856ee 100644 --- a/beszel/internal/records/records.go +++ b/beszel/internal/records/records.go @@ -206,12 +206,16 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.LoadAvg1 += stats.LoadAvg1 sum.LoadAvg5 += stats.LoadAvg5 sum.LoadAvg15 += stats.LoadAvg15 + sum.Bandwidth[0] += stats.Bandwidth[0] + sum.Bandwidth[1] += stats.Bandwidth[1] // Set peak values sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu) sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent) sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv) sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs) sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs) + sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0]) + sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1]) // Accumulate temperatures if stats.Temperatures != nil { @@ -284,6 +288,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count) sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count) sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count) + sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count) + sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count) // Average temperatures if sum.Temperatures != nil && tempCount > 0 { for key := range sum.Temperatures { diff --git a/beszel/site/src/components/charts/area-chart.tsx b/beszel/site/src/components/charts/area-chart.tsx index 814ad28..14c4a43 100644 --- a/beszel/site/src/components/charts/area-chart.tsx +++ b/beszel/site/src/components/charts/area-chart.tsx @@ -1,128 +1,91 @@ -import { t } from "@lingui/core/macro" - import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils" -// import Spinner from '../spinner' -import { ChartData } from "@/types" -import { memo, useMemo } from "react" -import { useLingui } from "@lingui/react/macro" +import { ChartData, SystemStatsRecord } from "@/types" +import { useMemo } from "react" -/** [label, key, color, opacity] */ -type DataKeys = [string, string, number, number] - -const getNestedValue = (path: string, max = false, data: any): number | null => { - // fallback value (obj?.stats?.cpum ? 0 : null) should only come into play when viewing - // a max value which doesn't exist, or the value was zero and omitted from the stats object. - // so we check if cpum is present. if so, return 0 to make sure the zero value is displayed. - // if not, return null - there is no max data so do not display anything. - return `stats.${path}${max ? "m" : ""}` - .split(".") - .reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data) +export type DataPoint = { + label: string + dataKey: (data: SystemStatsRecord) => number | undefined + color: string + opacity: number } -export default memo(function AreaChartDefault({ - maxToggled = false, - chartName, +export default function AreaChartDefault({ chartData, max, + maxToggled, tickFormatter, contentFormatter, -}: { - maxToggled?: boolean - chartName: string + dataPoints, +}: // logRender = false, +{ chartData: ChartData max?: number - tickFormatter: (value: number) => string - contentFormatter: ({ value }: { value: number }) => string + maxToggled?: boolean + tickFormatter: (value: number, index: number) => string + contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string + dataPoints?: DataPoint[] + // logRender?: boolean }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - const { i18n } = useLingui() - const { chartTime } = chartData - - const showMax = chartTime !== "1h" && maxToggled - - const dataKeys: DataKeys[] = useMemo(() => { - // [label, key, color, opacity] - if (chartName === "CPU Usage") { - return [[t`CPU Usage`, "cpu", 1, 0.4]] - } else if (chartName === "dio") { - return [ - [t({ message: "Write", comment: "Disk write" }), "dw", 3, 0.3], - [t({ message: "Read", comment: "Disk read" }), "dr", 1, 0.3], - ] - } else if (chartName === "bw") { - return [ - [t({ message: "Sent", comment: "Network bytes sent (upload)" }), "ns", 5, 0.2], - [t({ message: "Received", comment: "Network bytes received (download)" }), "nr", 2, 0.2], - ] - } else if (chartName.startsWith("efs")) { - return [ - [t`Write`, `${chartName}.w`, 3, 0.3], - [t`Read`, `${chartName}.r`, 1, 0.3], - ] - } else if (chartName.startsWith("g.")) { - return [chartName.includes("mu") ? [t`Used`, chartName, 2, 0.25] : [t`Usage`, chartName, 1, 0.4]] + return useMemo(() => { + if (chartData.systemStats.length === 0) { + return null } - return [] - }, [chartName, i18n.locale]) - - // console.log('Rendered at', new Date()) - - if (chartData.systemStats.length === 0) { - return null - } - - return ( -