mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-21 21:26:16 +01:00
ensure deprecated system fields are migrated to newer structures
also removes refs to legacy load avg fields (l1, l5, l15) that were around for a very short period
This commit is contained in:
@@ -213,10 +213,8 @@ func (a *Agent) applyNetworkTotals(
|
|||||||
totalBytesSent, totalBytesRecv uint64,
|
totalBytesSent, totalBytesRecv uint64,
|
||||||
bytesSentPerSecond, bytesRecvPerSecond uint64,
|
bytesSentPerSecond, bytesRecvPerSecond uint64,
|
||||||
) {
|
) {
|
||||||
networkSentPs := utils.BytesToMegabytes(float64(bytesSentPerSecond))
|
if bytesSentPerSecond > 10_000_000_000 || bytesRecvPerSecond > 10_000_000_000 {
|
||||||
networkRecvPs := utils.BytesToMegabytes(float64(bytesRecvPerSecond))
|
slog.Warn("Invalid net stats. Resetting.", "sent", bytesSentPerSecond, "recv", bytesRecvPerSecond)
|
||||||
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
|
||||||
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
|
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||||
continue
|
continue
|
||||||
@@ -226,14 +224,10 @@ func (a *Agent) applyNetworkTotals(
|
|||||||
a.initializeNetIoStats()
|
a.initializeNetIoStats()
|
||||||
delete(a.netIoStats, cacheTimeMs)
|
delete(a.netIoStats, cacheTimeMs)
|
||||||
delete(a.netInterfaceDeltaTrackers, cacheTimeMs)
|
delete(a.netInterfaceDeltaTrackers, cacheTimeMs)
|
||||||
systemStats.NetworkSent = 0
|
|
||||||
systemStats.NetworkRecv = 0
|
|
||||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0
|
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
systemStats.NetworkSent = networkSentPs
|
|
||||||
systemStats.NetworkRecv = networkRecvPs
|
|
||||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
||||||
nis.BytesSent = totalBytesSent
|
nis.BytesSent = totalBytesSent
|
||||||
nis.BytesRecv = totalBytesRecv
|
nis.BytesRecv = totalBytesRecv
|
||||||
|
|||||||
@@ -416,8 +416,6 @@ func TestApplyNetworkTotals(t *testing.T) {
|
|||||||
totalBytesSent uint64
|
totalBytesSent uint64
|
||||||
totalBytesRecv uint64
|
totalBytesRecv uint64
|
||||||
expectReset bool
|
expectReset bool
|
||||||
expectedNetworkSent float64
|
|
||||||
expectedNetworkRecv float64
|
|
||||||
expectedBandwidthSent uint64
|
expectedBandwidthSent uint64
|
||||||
expectedBandwidthRecv uint64
|
expectedBandwidthRecv uint64
|
||||||
}{
|
}{
|
||||||
@@ -428,8 +426,6 @@ func TestApplyNetworkTotals(t *testing.T) {
|
|||||||
totalBytesSent: 10000000,
|
totalBytesSent: 10000000,
|
||||||
totalBytesRecv: 20000000,
|
totalBytesRecv: 20000000,
|
||||||
expectReset: false,
|
expectReset: false,
|
||||||
expectedNetworkSent: 0.95, // ~1 MB/s rounded to 2 decimals
|
|
||||||
expectedNetworkRecv: 1.91, // ~2 MB/s rounded to 2 decimals
|
|
||||||
expectedBandwidthSent: 1000000,
|
expectedBandwidthSent: 1000000,
|
||||||
expectedBandwidthRecv: 2000000,
|
expectedBandwidthRecv: 2000000,
|
||||||
},
|
},
|
||||||
@@ -457,18 +453,6 @@ func TestApplyNetworkTotals(t *testing.T) {
|
|||||||
totalBytesRecv: 20000000,
|
totalBytesRecv: 20000000,
|
||||||
expectReset: true,
|
expectReset: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Valid network stats - at threshold boundary",
|
|
||||||
bytesSentPerSecond: 10485750000, // ~9999.99 MB/s (rounds to 9999.99)
|
|
||||||
bytesRecvPerSecond: 10485750000, // ~9999.99 MB/s (rounds to 9999.99)
|
|
||||||
totalBytesSent: 10000000,
|
|
||||||
totalBytesRecv: 20000000,
|
|
||||||
expectReset: false,
|
|
||||||
expectedNetworkSent: 9999.99,
|
|
||||||
expectedNetworkRecv: 9999.99,
|
|
||||||
expectedBandwidthSent: 10485750000,
|
|
||||||
expectedBandwidthRecv: 10485750000,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Zero values",
|
name: "Zero values",
|
||||||
bytesSentPerSecond: 0,
|
bytesSentPerSecond: 0,
|
||||||
@@ -476,8 +460,6 @@ func TestApplyNetworkTotals(t *testing.T) {
|
|||||||
totalBytesSent: 0,
|
totalBytesSent: 0,
|
||||||
totalBytesRecv: 0,
|
totalBytesRecv: 0,
|
||||||
expectReset: false,
|
expectReset: false,
|
||||||
expectedNetworkSent: 0.0,
|
|
||||||
expectedNetworkRecv: 0.0,
|
|
||||||
expectedBandwidthSent: 0,
|
expectedBandwidthSent: 0,
|
||||||
expectedBandwidthRecv: 0,
|
expectedBandwidthRecv: 0,
|
||||||
},
|
},
|
||||||
@@ -514,14 +496,10 @@ func TestApplyNetworkTotals(t *testing.T) {
|
|||||||
// Should have reset network tracking state - maps cleared and stats zeroed
|
// Should have reset network tracking state - maps cleared and stats zeroed
|
||||||
assert.NotContains(t, a.netIoStats, cacheTimeMs, "cache entry should be cleared after reset")
|
assert.NotContains(t, a.netIoStats, cacheTimeMs, "cache entry should be cleared after reset")
|
||||||
assert.NotContains(t, a.netInterfaceDeltaTrackers, cacheTimeMs, "tracker should be cleared on reset")
|
assert.NotContains(t, a.netInterfaceDeltaTrackers, cacheTimeMs, "tracker should be cleared on reset")
|
||||||
assert.Zero(t, systemStats.NetworkSent)
|
|
||||||
assert.Zero(t, systemStats.NetworkRecv)
|
|
||||||
assert.Zero(t, systemStats.Bandwidth[0])
|
assert.Zero(t, systemStats.Bandwidth[0])
|
||||||
assert.Zero(t, systemStats.Bandwidth[1])
|
assert.Zero(t, systemStats.Bandwidth[1])
|
||||||
} else {
|
} else {
|
||||||
// Should have applied stats
|
// Should have applied stats
|
||||||
assert.Equal(t, tt.expectedNetworkSent, systemStats.NetworkSent)
|
|
||||||
assert.Equal(t, tt.expectedNetworkRecv, systemStats.NetworkRecv)
|
|
||||||
assert.Equal(t, tt.expectedBandwidthSent, systemStats.Bandwidth[0])
|
assert.Equal(t, tt.expectedBandwidthSent, systemStats.Bandwidth[0])
|
||||||
assert.Equal(t, tt.expectedBandwidthRecv, systemStats.Bandwidth[1])
|
assert.Equal(t, tt.expectedBandwidthRecv, systemStats.Bandwidth[1])
|
||||||
|
|
||||||
|
|||||||
@@ -45,17 +45,17 @@ type SystemAlertFsStats struct {
|
|||||||
DiskUsed float64 `json:"du"`
|
DiskUsed float64 `json:"du"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Values pulled from system_stats.stats that are relevant to alerts.
|
||||||
type SystemAlertStats struct {
|
type SystemAlertStats struct {
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
Mem float64 `json:"mp"`
|
Mem float64 `json:"mp"`
|
||||||
Disk float64 `json:"dp"`
|
Disk float64 `json:"dp"`
|
||||||
NetSent float64 `json:"ns"`
|
Bandwidth [2]uint64 `json:"b"`
|
||||||
NetRecv float64 `json:"nr"`
|
GPU map[string]SystemAlertGPUData `json:"g"`
|
||||||
GPU map[string]SystemAlertGPUData `json:"g"`
|
Temperatures map[string]float32 `json:"t"`
|
||||||
Temperatures map[string]float32 `json:"t"`
|
LoadAvg [3]float64 `json:"la"`
|
||||||
LoadAvg [3]float64 `json:"la"`
|
Battery [2]uint8 `json:"bat"`
|
||||||
Battery [2]uint8 `json:"bat"`
|
ExtraFs map[string]SystemAlertFsStats `json:"efs"`
|
||||||
ExtraFs map[string]SystemAlertFsStats `json:"efs"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemAlertGPUData struct {
|
type SystemAlertGPUData struct {
|
||||||
@@ -265,13 +265,14 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add link
|
// Add link
|
||||||
if scheme == "ntfy" {
|
switch scheme {
|
||||||
|
case "ntfy":
|
||||||
queryParams.Add("Actions", fmt.Sprintf("view, %s, %s", linkText, link))
|
queryParams.Add("Actions", fmt.Sprintf("view, %s, %s", linkText, link))
|
||||||
} else if scheme == "lark" {
|
case "lark":
|
||||||
queryParams.Add("link", link)
|
queryParams.Add("link", link)
|
||||||
} else if scheme == "bark" {
|
case "bark":
|
||||||
queryParams.Add("url", link)
|
queryParams.Add("url", link)
|
||||||
} else {
|
default:
|
||||||
message += "\n\n" + link
|
message += "\n\n" + link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/tools/types"
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
"github.com/spf13/cast"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {
|
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {
|
||||||
@@ -92,7 +91,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
min := max(1, uint8(alertRecord.GetInt("min")))
|
||||||
|
|
||||||
alert := SystemAlertData{
|
alert := SystemAlertData{
|
||||||
systemRecord: systemRecord,
|
systemRecord: systemRecord,
|
||||||
@@ -192,7 +191,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
case "Memory":
|
case "Memory":
|
||||||
alert.val += stats.Mem
|
alert.val += stats.Mem
|
||||||
case "Bandwidth":
|
case "Bandwidth":
|
||||||
alert.val += stats.NetSent + stats.NetRecv
|
alert.val += float64(stats.Bandwidth[0]+stats.Bandwidth[1]) / (1024 * 1024)
|
||||||
case "Disk":
|
case "Disk":
|
||||||
if alert.mapSums == nil {
|
if alert.mapSums == nil {
|
||||||
alert.mapSums = make(map[string]float32, len(stats.ExtraFs)+1)
|
alert.mapSums = make(map[string]float32, len(stats.ExtraFs)+1)
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import (
|
|||||||
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||||
MaxCpu float64 `json:"cpum,omitempty" cbor:"1,keyasint,omitempty"`
|
MaxCpu float64 `json:"cpum,omitempty" cbor:"-"`
|
||||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||||
|
MaxMem float64 `json:"mm,omitempty" cbor:"-"`
|
||||||
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
||||||
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
||||||
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
||||||
@@ -23,26 +24,25 @@ type Stats struct {
|
|||||||
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
||||||
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
||||||
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
||||||
DiskReadPs float64 `json:"dr" cbor:"12,keyasint"`
|
DiskReadPs float64 `json:"dr,omitzero" cbor:"12,keyasint,omitzero"`
|
||||||
DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
|
DiskWritePs float64 `json:"dw,omitzero" cbor:"13,keyasint,omitzero"`
|
||||||
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
|
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"-"`
|
||||||
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
|
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"-"`
|
||||||
NetworkSent float64 `json:"ns,omitzero" cbor:"16,keyasint,omitzero"`
|
NetworkSent float64 `json:"ns,omitzero" cbor:"16,keyasint,omitzero"`
|
||||||
NetworkRecv float64 `json:"nr,omitzero" cbor:"17,keyasint,omitzero"`
|
NetworkRecv float64 `json:"nr,omitzero" cbor:"17,keyasint,omitzero"`
|
||||||
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"18,keyasint,omitempty"`
|
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"-"`
|
||||||
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"19,keyasint,omitempty"`
|
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"-"`
|
||||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
||||||
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
|
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
|
||||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
|
// LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
|
||||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
|
// LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
|
||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,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]
|
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]
|
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"-"` // [sent bytes, recv bytes]
|
||||||
// TODO: remove other load fields in future release in favor of load avg array
|
// TODO: remove other load fields in future release in favor of load avg array
|
||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
|
||||||
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
||||||
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
|
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
|
||||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
||||||
@@ -90,8 +90,8 @@ type FsStats struct {
|
|||||||
TotalWrite uint64 `json:"-"`
|
TotalWrite uint64 `json:"-"`
|
||||||
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
||||||
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
||||||
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"4,keyasint,omitempty"`
|
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
|
||||||
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"5,keyasint,omitempty"`
|
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
|
||||||
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
|
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
|
||||||
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
|
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
|
||||||
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
|
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
|
||||||
@@ -129,23 +129,23 @@ type Info struct {
|
|||||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
|
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
|
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
|
||||||
// Threads is needed in Info struct to calculate load average thresholds
|
// Threads is needed in Info struct to calculate load average thresholds
|
||||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||||
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
|
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||||
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
|
Bandwidth float64 `json:"b,omitzero" cbor:"9,keyasint"` // deprecated in favor of BandwidthBytes
|
||||||
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||||
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
|
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||||
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
|
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
|
// LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
|
// LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
|
// LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
|
||||||
|
|
||||||
|
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ func (sys *System) update() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure deprecated fields from older agents are migrated to current fields
|
||||||
|
migrateDeprecatedFields(data, !sys.detailsFetched.Load())
|
||||||
|
|
||||||
// create system records
|
// create system records
|
||||||
_, err = sys.createRecords(data)
|
_, err = sys.createRecords(data)
|
||||||
|
|
||||||
@@ -702,3 +705,50 @@ func getJitter() <-chan time.Time {
|
|||||||
msDelay := (interval * minPercent / 100) + rand.Intn(interval*jitterRange/100)
|
msDelay := (interval * minPercent / 100) + rand.Intn(interval*jitterRange/100)
|
||||||
return time.After(time.Duration(msDelay) * time.Millisecond)
|
return time.After(time.Duration(msDelay) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateDeprecatedFields moves values from deprecated fields to their new locations if the new
|
||||||
|
// fields are not already populated. Deprecated fields and refs may be removed at least 30 days
|
||||||
|
// and one minor version release after the release that includes the migration.
|
||||||
|
//
|
||||||
|
// This is run when processing incoming system data from agents, which may be on older versions.
|
||||||
|
func migrateDeprecatedFields(cd *system.CombinedData, createDetails bool) {
|
||||||
|
// migration added 0.19.0
|
||||||
|
if cd.Stats.Bandwidth[0] == 0 && cd.Stats.Bandwidth[1] == 0 {
|
||||||
|
cd.Stats.Bandwidth[0] = uint64(cd.Stats.NetworkSent * 1024 * 1024)
|
||||||
|
cd.Stats.Bandwidth[1] = uint64(cd.Stats.NetworkRecv * 1024 * 1024)
|
||||||
|
cd.Stats.NetworkSent, cd.Stats.NetworkRecv = 0, 0
|
||||||
|
}
|
||||||
|
// migration added 0.19.0
|
||||||
|
if cd.Info.BandwidthBytes == 0 {
|
||||||
|
cd.Info.BandwidthBytes = uint64(cd.Info.Bandwidth * 1024 * 1024)
|
||||||
|
cd.Info.Bandwidth = 0
|
||||||
|
}
|
||||||
|
// migration added 0.19.0
|
||||||
|
if cd.Stats.DiskIO[0] == 0 && cd.Stats.DiskIO[1] == 0 {
|
||||||
|
cd.Stats.DiskIO[0] = uint64(cd.Stats.DiskReadPs * 1024 * 1024)
|
||||||
|
cd.Stats.DiskIO[1] = uint64(cd.Stats.DiskWritePs * 1024 * 1024)
|
||||||
|
cd.Stats.DiskReadPs, cd.Stats.DiskWritePs = 0, 0
|
||||||
|
}
|
||||||
|
// migration added 0.19.0 - Move deprecated Info fields to Details struct
|
||||||
|
if cd.Details == nil && cd.Info.Hostname != "" {
|
||||||
|
if createDetails {
|
||||||
|
cd.Details = &system.Details{
|
||||||
|
Hostname: cd.Info.Hostname,
|
||||||
|
Kernel: cd.Info.KernelVersion,
|
||||||
|
Cores: cd.Info.Cores,
|
||||||
|
Threads: cd.Info.Threads,
|
||||||
|
CpuModel: cd.Info.CpuModel,
|
||||||
|
Podman: cd.Info.Podman,
|
||||||
|
Os: cd.Info.Os,
|
||||||
|
MemoryTotal: uint64(cd.Stats.Mem * 1024 * 1024 * 1024),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// zero the deprecated fields to prevent saving them in systems.info DB json payload
|
||||||
|
cd.Info.Hostname = ""
|
||||||
|
cd.Info.KernelVersion = ""
|
||||||
|
cd.Info.Cores = 0
|
||||||
|
cd.Info.CpuModel = ""
|
||||||
|
cd.Info.Podman = false
|
||||||
|
cd.Info.Os = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
159
internal/hub/systems/system_test.go
Normal file
159
internal/hub/systems/system_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
//go:build testing
|
||||||
|
|
||||||
|
package systems
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCombinedData_MigrateDeprecatedFields(t *testing.T) {
|
||||||
|
t.Run("Migrate NetworkSent and NetworkRecv to Bandwidth", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Stats: system.Stats{
|
||||||
|
NetworkSent: 1.5, // 1.5 MB
|
||||||
|
NetworkRecv: 2.5, // 2.5 MB
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, true)
|
||||||
|
|
||||||
|
expectedSent := uint64(1.5 * 1024 * 1024)
|
||||||
|
expectedRecv := uint64(2.5 * 1024 * 1024)
|
||||||
|
|
||||||
|
if cd.Stats.Bandwidth[0] != expectedSent {
|
||||||
|
t.Errorf("expected Bandwidth[0] %d, got %d", expectedSent, cd.Stats.Bandwidth[0])
|
||||||
|
}
|
||||||
|
if cd.Stats.Bandwidth[1] != expectedRecv {
|
||||||
|
t.Errorf("expected Bandwidth[1] %d, got %d", expectedRecv, cd.Stats.Bandwidth[1])
|
||||||
|
}
|
||||||
|
if cd.Stats.NetworkSent != 0 || cd.Stats.NetworkRecv != 0 {
|
||||||
|
t.Errorf("expected NetworkSent and NetworkRecv to be reset, got %f, %f", cd.Stats.NetworkSent, cd.Stats.NetworkRecv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Migrate Info.Bandwidth to Info.BandwidthBytes", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Info: system.Info{
|
||||||
|
Bandwidth: 10.0, // 10 MB
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, true)
|
||||||
|
|
||||||
|
expected := uint64(10 * 1024 * 1024)
|
||||||
|
if cd.Info.BandwidthBytes != expected {
|
||||||
|
t.Errorf("expected BandwidthBytes %d, got %d", expected, cd.Info.BandwidthBytes)
|
||||||
|
}
|
||||||
|
if cd.Info.Bandwidth != 0 {
|
||||||
|
t.Errorf("expected Info.Bandwidth to be reset, got %f", cd.Info.Bandwidth)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Migrate DiskReadPs and DiskWritePs to DiskIO", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Stats: system.Stats{
|
||||||
|
DiskReadPs: 3.0, // 3 MB
|
||||||
|
DiskWritePs: 4.0, // 4 MB
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, true)
|
||||||
|
|
||||||
|
expectedRead := uint64(3 * 1024 * 1024)
|
||||||
|
expectedWrite := uint64(4 * 1024 * 1024)
|
||||||
|
|
||||||
|
if cd.Stats.DiskIO[0] != expectedRead {
|
||||||
|
t.Errorf("expected DiskIO[0] %d, got %d", expectedRead, cd.Stats.DiskIO[0])
|
||||||
|
}
|
||||||
|
if cd.Stats.DiskIO[1] != expectedWrite {
|
||||||
|
t.Errorf("expected DiskIO[1] %d, got %d", expectedWrite, cd.Stats.DiskIO[1])
|
||||||
|
}
|
||||||
|
if cd.Stats.DiskReadPs != 0 || cd.Stats.DiskWritePs != 0 {
|
||||||
|
t.Errorf("expected DiskReadPs and DiskWritePs to be reset, got %f, %f", cd.Stats.DiskReadPs, cd.Stats.DiskWritePs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Migrate Info fields to Details struct", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Stats: system.Stats{
|
||||||
|
Mem: 16.0, // 16 GB
|
||||||
|
},
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
KernelVersion: "6.8.0",
|
||||||
|
Cores: 8,
|
||||||
|
Threads: 16,
|
||||||
|
CpuModel: "Intel i7",
|
||||||
|
Podman: true,
|
||||||
|
Os: system.Linux,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, true)
|
||||||
|
|
||||||
|
if cd.Details == nil {
|
||||||
|
t.Fatal("expected Details struct to be created")
|
||||||
|
}
|
||||||
|
if cd.Details.Hostname != "test-host" {
|
||||||
|
t.Errorf("expected Hostname 'test-host', got '%s'", cd.Details.Hostname)
|
||||||
|
}
|
||||||
|
if cd.Details.Kernel != "6.8.0" {
|
||||||
|
t.Errorf("expected Kernel '6.8.0', got '%s'", cd.Details.Kernel)
|
||||||
|
}
|
||||||
|
if cd.Details.Cores != 8 {
|
||||||
|
t.Errorf("expected Cores 8, got %d", cd.Details.Cores)
|
||||||
|
}
|
||||||
|
if cd.Details.Threads != 16 {
|
||||||
|
t.Errorf("expected Threads 16, got %d", cd.Details.Threads)
|
||||||
|
}
|
||||||
|
if cd.Details.CpuModel != "Intel i7" {
|
||||||
|
t.Errorf("expected CpuModel 'Intel i7', got '%s'", cd.Details.CpuModel)
|
||||||
|
}
|
||||||
|
if cd.Details.Podman != true {
|
||||||
|
t.Errorf("expected Podman true, got %v", cd.Details.Podman)
|
||||||
|
}
|
||||||
|
if cd.Details.Os != system.Linux {
|
||||||
|
t.Errorf("expected Os Linux, got %d", cd.Details.Os)
|
||||||
|
}
|
||||||
|
expectedMem := uint64(16 * 1024 * 1024 * 1024)
|
||||||
|
if cd.Details.MemoryTotal != expectedMem {
|
||||||
|
t.Errorf("expected MemoryTotal %d, got %d", expectedMem, cd.Details.MemoryTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cd.Info.Hostname != "" || cd.Info.KernelVersion != "" || cd.Info.Cores != 0 || cd.Info.CpuModel != "" || cd.Info.Podman != false || cd.Info.Os != 0 {
|
||||||
|
t.Errorf("expected Info fields to be reset, got %+v", cd.Info)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Do not migrate if Details already exists", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Details: &system.Details{Hostname: "existing-host"},
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "deprecated-host",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, true)
|
||||||
|
|
||||||
|
if cd.Details.Hostname != "existing-host" {
|
||||||
|
t.Errorf("expected Hostname 'existing-host', got '%s'", cd.Details.Hostname)
|
||||||
|
}
|
||||||
|
if cd.Info.Hostname != "deprecated-host" {
|
||||||
|
t.Errorf("expected Info.Hostname to remain 'deprecated-host', got '%s'", cd.Info.Hostname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Do not create details if migrateDetails is false", func(t *testing.T) {
|
||||||
|
cd := &system.CombinedData{
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "deprecated-host",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
migrateDeprecatedFields(cd, false)
|
||||||
|
|
||||||
|
if cd.Details != nil {
|
||||||
|
t.Fatal("expected Details struct to not be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cd.Info.Hostname != "" {
|
||||||
|
t.Errorf("expected Info.Hostname to be reset, got '%s'", cd.Info.Hostname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -16,19 +16,16 @@ import { useYAxisWidth } from "./hooks"
|
|||||||
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
const keys: { legacy: keyof SystemStats; color: string; label: string }[] = [
|
const keys: { color: string; label: string }[] = [
|
||||||
{
|
{
|
||||||
legacy: "l1",
|
|
||||||
color: "hsl(271, 81%, 60%)", // Purple
|
color: "hsl(271, 81%, 60%)", // Purple
|
||||||
label: t({ message: `1 min`, comment: "Load average" }),
|
label: t({ message: `1 min`, comment: "Load average" }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
legacy: "l5",
|
|
||||||
color: "hsl(217, 91%, 60%)", // Blue
|
color: "hsl(217, 91%, 60%)", // Blue
|
||||||
label: t({ message: `5 min`, comment: "Load average" }),
|
label: t({ message: `5 min`, comment: "Load average" }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
legacy: "l15",
|
|
||||||
color: "hsl(25, 95%, 53%)", // Orange
|
color: "hsl(25, 95%, 53%)", // Orange
|
||||||
label: t({ message: `15 min`, comment: "Load average" }),
|
label: t({ message: `15 min`, comment: "Load average" }),
|
||||||
},
|
},
|
||||||
@@ -66,27 +63,18 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{keys.map(({ legacy, color, label }, i) => {
|
{keys.map(({ color, label }, i) => (
|
||||||
const dataKey = (value: { stats: SystemStats }) => {
|
<Line
|
||||||
const { minor, patch } = chartData.agentVersion
|
key={label}
|
||||||
if (minor <= 12 && patch < 1) {
|
dataKey={(value: { stats: SystemStats }) => value.stats?.la?.[i]}
|
||||||
return value.stats?.[legacy]
|
name={label}
|
||||||
}
|
type="monotoneX"
|
||||||
return value.stats?.la?.[i] ?? value.stats?.[legacy]
|
dot={false}
|
||||||
}
|
strokeWidth={1.5}
|
||||||
return (
|
stroke={color}
|
||||||
<Line
|
isAnimationActive={false}
|
||||||
key={label}
|
/>
|
||||||
dataKey={dataKey}
|
))}
|
||||||
name={label}
|
|
||||||
type="monotoneX"
|
|
||||||
dot={false}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke={color}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Load Average chart */}
|
{/* Load Average chart */}
|
||||||
{chartData.agentVersion?.minor >= 12 && (
|
{chartData.agentVersion?.minor > 12 && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
|
|||||||
@@ -198,32 +198,19 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "loadAverage",
|
id: "loadAverage",
|
||||||
accessorFn: ({ info }) => {
|
accessorFn: ({ info }) => info.la?.reduce((acc, curr) => acc + curr, 0),
|
||||||
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) || undefined
|
|
||||||
}
|
|
||||||
return sum || undefined
|
|
||||||
},
|
|
||||||
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
|
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
|
||||||
size: 0,
|
size: 0,
|
||||||
Icon: HourglassIcon,
|
Icon: HourglassIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
cell(info: CellContext<SystemRecord, unknown>) {
|
cell(info: CellContext<SystemRecord, unknown>) {
|
||||||
const { info: sysInfo, status } = info.row.original
|
const { info: sysInfo, status } = info.row.original
|
||||||
|
const { major, minor } = parseSemVer(sysInfo.v)
|
||||||
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
|
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
|
||||||
// agent version
|
const loadAverages = sysInfo.la || []
|
||||||
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)
|
const max = Math.max(...loadAverages)
|
||||||
if (max === 0 && (status === SystemStatus.Paused || minor < 12)) {
|
if (max === 0 && (status === SystemStatus.Paused || (major < 1 && minor < 13))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,19 +235,20 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024 || undefined,
|
accessorFn: ({ info, status }) => (status !== SystemStatus.Up ? undefined : info.bb),
|
||||||
id: "net",
|
id: "net",
|
||||||
name: () => t`Net`,
|
name: () => t`Net`,
|
||||||
size: 0,
|
size: 0,
|
||||||
Icon: EthernetIcon,
|
Icon: EthernetIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
|
sortUndefined: "last",
|
||||||
cell(info) {
|
cell(info) {
|
||||||
const sys = info.row.original
|
const val = info.getValue() as number | undefined
|
||||||
const userSettings = useStore($userSettings, { keys: ["unitNet"] })
|
if (val === undefined) {
|
||||||
if (sys.status === SystemStatus.Paused) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { value, unit } = formatBytes((info.getValue() || 0) as number, true, userSettings.unitNet, false)
|
const userSettings = useStore($userSettings, { keys: ["unitNet"] })
|
||||||
|
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
|
||||||
return (
|
return (
|
||||||
<span className="tabular-nums whitespace-nowrap">
|
<span className="tabular-nums whitespace-nowrap">
|
||||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||||
|
|||||||
13
internal/site/src/types.d.ts
vendored
13
internal/site/src/types.d.ts
vendored
@@ -45,12 +45,6 @@ export interface SystemInfo {
|
|||||||
c: number
|
c: number
|
||||||
/** cpu model */
|
/** cpu model */
|
||||||
m: string
|
m: string
|
||||||
/** load average 1 minute */
|
|
||||||
l1?: number
|
|
||||||
/** load average 5 minutes */
|
|
||||||
l5?: number
|
|
||||||
/** load average 15 minutes */
|
|
||||||
l15?: number
|
|
||||||
/** load average */
|
/** load average */
|
||||||
la?: [number, number, number]
|
la?: [number, number, number]
|
||||||
/** operating system */
|
/** operating system */
|
||||||
@@ -94,13 +88,6 @@ export interface SystemStats {
|
|||||||
cpub?: number[]
|
cpub?: number[]
|
||||||
/** per-core cpu usage [CPU0..] (0-100 integers) */
|
/** per-core cpu usage [CPU0..] (0-100 integers) */
|
||||||
cpus?: number[]
|
cpus?: 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 */
|
/** load average */
|
||||||
la?: [number, number, number]
|
la?: [number, number, number]
|
||||||
/** total memory (gb) */
|
/** total memory (gb) */
|
||||||
|
|||||||
Reference in New Issue
Block a user