mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-21 21:26:16 +01:00
* collect OS info * Fix systeminfo * Fix it * optimize it * Add disk info * add ethernet info * add ethernet * remove speed from ethernet * add cpu info * chore cleanup data * chore fix podman * restruct systeminfo * use short cpu name * debug memory * collect and show memory * remove os from the table * truncate nic name * chore: shorter names in json * collect memory info * add debug * undo memory * revert package.json * fix conflicts * fix conflixts * Fix MacOs os family * add ISP data for remote systems * reorder the system page bar information * remove OS from the system table * Update with main * Fix vulcheck * Fix systembar * fix system bar * fix vulcheck * update struct with static info * Adjust collection method to upon agent connection
449 lines
12 KiB
Go
449 lines
12 KiB
Go
package agent
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/henrygd/beszel"
|
|
"github.com/henrygd/beszel/agent/battery"
|
|
"github.com/henrygd/beszel/internal/entities/system"
|
|
"github.com/jaypipes/ghw/pkg/block"
|
|
ghwnet "github.com/jaypipes/ghw/pkg/net"
|
|
ghwpci "github.com/jaypipes/ghw/pkg/pci"
|
|
"github.com/shirou/gopsutil/v4/cpu"
|
|
"github.com/shirou/gopsutil/v4/host"
|
|
"github.com/shirou/gopsutil/v4/load"
|
|
"github.com/shirou/gopsutil/v4/mem"
|
|
)
|
|
|
|
// prevDisk stores previous per-device disk counters for a given cache interval
|
|
type prevDisk struct {
|
|
readBytes uint64
|
|
writeBytes uint64
|
|
at time.Time
|
|
}
|
|
|
|
// Sets initial / non-changing values about the host system
|
|
func (a *Agent) initializeSystemInfo() {
|
|
hostname, _ := os.Hostname()
|
|
a.staticSystemInfo.Hostname = hostname
|
|
a.staticSystemInfo.AgentVersion = beszel.Version
|
|
|
|
platform, family, version, _ := host.PlatformInformation()
|
|
|
|
var osFamily, osVersion, osKernel string
|
|
var osType system.Os
|
|
if platform == "darwin" {
|
|
osKernel = version
|
|
osFamily = "macOS" // macOS is the family name for Darwin
|
|
osVersion = version
|
|
} else if strings.Contains(platform, "indows") {
|
|
osKernel = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
|
|
osFamily = family
|
|
osVersion = version
|
|
osType = system.Windows
|
|
} else if platform == "freebsd" {
|
|
osKernel = version
|
|
osFamily = family
|
|
osVersion = version
|
|
} else {
|
|
osFamily = family
|
|
osVersion = version
|
|
osKernel = ""
|
|
osRelease := readOsRelease()
|
|
if pretty, ok := osRelease["PRETTY_NAME"]; ok {
|
|
osFamily = pretty
|
|
}
|
|
if name, ok := osRelease["NAME"]; ok {
|
|
osFamily = name
|
|
}
|
|
if versionId, ok := osRelease["VERSION_ID"]; ok {
|
|
osVersion = versionId
|
|
}
|
|
}
|
|
if osKernel == "" {
|
|
osKernel, _ = host.KernelVersion()
|
|
}
|
|
a.staticSystemInfo.KernelVersion = osKernel
|
|
a.staticSystemInfo.Os = osType
|
|
a.staticSystemInfo.Oses = []system.OsInfo{{
|
|
Family: osFamily,
|
|
Version: osVersion,
|
|
Kernel: osKernel,
|
|
}}
|
|
|
|
// cpu model
|
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
|
arch := runtime.GOARCH
|
|
totalCores := 0
|
|
totalThreads := 0
|
|
for _, cpuInfo := range info {
|
|
totalCores += int(cpuInfo.Cores)
|
|
totalThreads++
|
|
}
|
|
modelName := info[0].ModelName
|
|
if idx := strings.Index(modelName, "@"); idx > 0 {
|
|
modelName = strings.TrimSpace(modelName[:idx])
|
|
}
|
|
cpu := system.CpuInfo{
|
|
Model: modelName,
|
|
SpeedGHz: fmt.Sprintf("%.2f GHz", info[0].Mhz/1000),
|
|
Arch: arch,
|
|
Cores: totalCores,
|
|
Threads: totalThreads,
|
|
}
|
|
a.staticSystemInfo.Cpus = []system.CpuInfo{cpu}
|
|
a.staticSystemInfo.Threads = totalThreads
|
|
slog.Debug("CPU info populated", "cpus", a.staticSystemInfo.Cpus)
|
|
}
|
|
|
|
// zfs
|
|
if _, err := getARCSize(); err != nil {
|
|
slog.Debug("Not monitoring ZFS ARC", "err", err)
|
|
} else {
|
|
a.zfs = true
|
|
}
|
|
|
|
// Collect disk info (model/vendor)
|
|
a.staticSystemInfo.Disks = getDiskInfo()
|
|
|
|
// Collect network interface info
|
|
a.staticSystemInfo.Networks = getNetworkInfo()
|
|
|
|
// Collect total memory and store in staticSystemInfo.Memory
|
|
if v, err := mem.VirtualMemory(); err == nil {
|
|
total := fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
|
|
a.staticSystemInfo.Memory = []system.MemoryInfo{{Total: total}}
|
|
slog.Debug("Memory info populated", "memory", a.staticSystemInfo.Memory)
|
|
}
|
|
|
|
}
|
|
|
|
// readPrettyName reads the PRETTY_NAME from /etc/os-release
|
|
func readPrettyName() string {
|
|
file, err := os.Open("/etc/os-release")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.HasPrefix(line, "PRETTY_NAME=") {
|
|
// Remove the prefix and any surrounding quotes
|
|
prettyName := strings.TrimPrefix(line, "PRETTY_NAME=")
|
|
prettyName = strings.Trim(prettyName, `"`)
|
|
return prettyName
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Returns current info, stats about the host system
|
|
func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|
var systemStats system.Stats
|
|
|
|
// battery
|
|
if batteryPercent, batteryState, err := battery.GetBatteryStats(); err == nil {
|
|
systemStats.Battery[0] = batteryPercent
|
|
systemStats.Battery[1] = batteryState
|
|
}
|
|
|
|
// cpu metrics
|
|
cpuMetrics, err := getCpuMetrics(cacheTimeMs)
|
|
if err == nil {
|
|
systemStats.Cpu = twoDecimals(cpuMetrics.Total)
|
|
systemStats.CpuBreakdown = []float64{
|
|
twoDecimals(cpuMetrics.User),
|
|
twoDecimals(cpuMetrics.System),
|
|
twoDecimals(cpuMetrics.Iowait),
|
|
twoDecimals(cpuMetrics.Steal),
|
|
twoDecimals(cpuMetrics.Idle),
|
|
}
|
|
} else {
|
|
slog.Error("Error getting cpu metrics", "err", err)
|
|
}
|
|
|
|
// per-core cpu usage
|
|
if perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {
|
|
systemStats.CpuCoresUsage = perCoreUsage
|
|
}
|
|
|
|
// load average
|
|
if avgstat, err := load.Avg(); err == nil {
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
// note: gopsutil automatically adds SReclaimable to v.Cached
|
|
cacheBuff := v.Cached + v.Buffers - v.Shared
|
|
if cacheBuff <= 0 {
|
|
cacheBuff = max(v.Total-v.Free-v.Used, 0)
|
|
}
|
|
// htop memory calculation overrides (likely outdated as of mid 2025)
|
|
if a.memCalc == "htop" {
|
|
// cacheBuff = v.Cached + v.Buffers - v.Shared
|
|
v.Used = v.Total - (v.Free + cacheBuff)
|
|
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
|
}
|
|
// if a.memCalc == "legacy" {
|
|
// v.Used = v.Total - v.Free - v.Buffers - v.Cached
|
|
// cacheBuff = v.Total - v.Free - v.Used
|
|
// 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.MemBuffCache = bytesToGigabytes(cacheBuff)
|
|
systemStats.MemUsed = bytesToGigabytes(v.Used)
|
|
systemStats.MemPct = twoDecimals(v.UsedPercent)
|
|
}
|
|
|
|
// disk usage
|
|
a.updateDiskUsage(&systemStats)
|
|
|
|
// disk i/o (cache-aware per interval)
|
|
a.updateDiskIo(cacheTimeMs, &systemStats)
|
|
|
|
// network stats (per cache interval)
|
|
a.updateNetworkStats(cacheTimeMs, &systemStats)
|
|
|
|
// temperatures
|
|
// TODO: maybe refactor to methods on systemStats
|
|
a.updateTemperatures(&systemStats)
|
|
|
|
// GPU data
|
|
if a.gpuManager != nil {
|
|
// reset high gpu percent
|
|
a.systemInfo.GpuPct = 0
|
|
// get current GPU data
|
|
if gpuData := a.gpuManager.GetCurrentData(cacheTimeMs); len(gpuData) > 0 {
|
|
systemStats.GPUData = gpuData
|
|
|
|
// add temperatures
|
|
if systemStats.Temperatures == nil {
|
|
systemStats.Temperatures = make(map[string]float64, len(gpuData))
|
|
}
|
|
highestTemp := 0.0
|
|
for _, gpu := range gpuData {
|
|
if gpu.Temperature > 0 {
|
|
systemStats.Temperatures[gpu.Name] = gpu.Temperature
|
|
if a.sensorConfig.primarySensor == gpu.Name {
|
|
a.systemInfo.DashboardTemp = gpu.Temperature
|
|
}
|
|
if gpu.Temperature > highestTemp {
|
|
highestTemp = gpu.Temperature
|
|
}
|
|
}
|
|
// update high gpu percent for dashboard
|
|
a.systemInfo.GpuPct = max(a.systemInfo.GpuPct, gpu.Usage)
|
|
}
|
|
// use highest temp for dashboard temp if dashboard temp is unset
|
|
if a.systemInfo.DashboardTemp == 0 {
|
|
a.systemInfo.DashboardTemp = highestTemp
|
|
}
|
|
}
|
|
}
|
|
|
|
// update base system info
|
|
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
|
a.systemInfo.Cpu = systemStats.Cpu
|
|
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.Battery = systemStats.Battery
|
|
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
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
func getDiskInfo() []system.DiskInfo {
|
|
blockInfo, err := block.New()
|
|
if err != nil {
|
|
slog.Debug("Failed to get block info with ghw", "err", err)
|
|
return nil
|
|
}
|
|
|
|
var disks []system.DiskInfo
|
|
for _, disk := range blockInfo.Disks {
|
|
disks = append(disks, system.DiskInfo{
|
|
Name: disk.Name,
|
|
Model: disk.Model,
|
|
Vendor: disk.Vendor,
|
|
})
|
|
}
|
|
return disks
|
|
}
|
|
|
|
func getNetworkInfo() []system.NetworkInfo {
|
|
netInfo, err := ghwnet.New()
|
|
if err != nil {
|
|
slog.Debug("Failed to get network info with ghw", "err", err)
|
|
return nil
|
|
}
|
|
pciInfo, err := ghwpci.New()
|
|
if err != nil {
|
|
slog.Debug("Failed to get PCI info with ghw", "err", err)
|
|
}
|
|
|
|
var networks []system.NetworkInfo
|
|
for _, nic := range netInfo.NICs {
|
|
if nic.IsVirtual {
|
|
continue
|
|
}
|
|
var vendor, model string
|
|
if nic.PCIAddress != nil && pciInfo != nil {
|
|
for _, dev := range pciInfo.Devices {
|
|
if dev.Address == *nic.PCIAddress {
|
|
if dev.Vendor != nil {
|
|
vendor = dev.Vendor.Name
|
|
}
|
|
if dev.Product != nil {
|
|
model = dev.Product.Name
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
networks = append(networks, system.NetworkInfo{
|
|
Name: nic.Name,
|
|
Vendor: vendor,
|
|
Model: model,
|
|
})
|
|
}
|
|
return networks
|
|
}
|
|
|
|
// getInterfaceCapabilitiesFromGhw uses ghw library to get interface capabilities
|
|
func getInterfaceCapabilitiesFromGhw(nic *ghwnet.NIC) string {
|
|
// Use the speed information from ghw if available
|
|
if nic.Speed != "" {
|
|
return nic.Speed
|
|
}
|
|
|
|
// If no speed info from ghw, try to get interface type from name
|
|
return getInterfaceTypeFromName(nic.Name)
|
|
}
|
|
|
|
// getInterfaceTypeFromName tries to determine interface type from name
|
|
func getInterfaceTypeFromName(ifaceName string) string {
|
|
// Common interface naming patterns
|
|
switch {
|
|
case strings.HasPrefix(ifaceName, "eth"):
|
|
return "Ethernet"
|
|
case strings.HasPrefix(ifaceName, "en"):
|
|
return "Ethernet"
|
|
case strings.HasPrefix(ifaceName, "wlan"):
|
|
return "WiFi"
|
|
case strings.HasPrefix(ifaceName, "wl"):
|
|
return "WiFi"
|
|
case strings.HasPrefix(ifaceName, "usb"):
|
|
return "USB"
|
|
case strings.HasPrefix(ifaceName, "tun"):
|
|
return "Tunnel"
|
|
case strings.HasPrefix(ifaceName, "tap"):
|
|
return "TAP"
|
|
case strings.HasPrefix(ifaceName, "br"):
|
|
return "Bridge"
|
|
case strings.HasPrefix(ifaceName, "bond"):
|
|
return "Bond"
|
|
case strings.HasPrefix(ifaceName, "veth"):
|
|
return "Virtual Ethernet"
|
|
case strings.HasPrefix(ifaceName, "docker"):
|
|
return "Docker"
|
|
case strings.HasPrefix(ifaceName, "lo"):
|
|
return "Loopback"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func readOsRelease() map[string]string {
|
|
file, err := os.Open("/etc/os-release")
|
|
if err != nil {
|
|
return map[string]string{}
|
|
}
|
|
defer file.Close()
|
|
|
|
release := make(map[string]string)
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if i := strings.Index(line, "="); i > 0 {
|
|
key := line[:i]
|
|
val := strings.Trim(line[i+1:], `"`)
|
|
release[key] = val
|
|
}
|
|
}
|
|
return release
|
|
}
|
|
|
|
func getMemoryInfo() []system.MemoryInfo {
|
|
var total string
|
|
if v, err := mem.VirtualMemory(); err == nil {
|
|
total = fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
|
|
}
|
|
return []system.MemoryInfo{{
|
|
Total: total,
|
|
}}
|
|
}
|
|
|