mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 22:16:18 +01:00
[Feature] Expand system info bar to include memory, disk, CPU, and OS details (#952)
* 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
This commit is contained in:
@@ -22,6 +22,14 @@ import (
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
// StaticInfoIntervalMs defines the cache time threshold for including static system info
|
||||
// Requests with cache time >= this value will include static info (reduces bandwidth)
|
||||
// Note: uint16 max is 65535, so we can't use 15 minutes directly. The hub will make
|
||||
// periodic requests at this interval.
|
||||
StaticInfoIntervalMs uint16 = 60_001 // Just above the standard 60s interval
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
@@ -37,7 +45,8 @@ type Agent struct {
|
||||
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
|
||||
dockerManager *dockerManager // Manages Docker API requests
|
||||
sensorConfig *SensorConfig // Sensors config
|
||||
systemInfo system.Info // Host system info
|
||||
systemInfo system.Info // Host system info (dynamic dashboard data)
|
||||
staticSystemInfo system.StaticInfo // Static system info (collected at longer intervals)
|
||||
gpuManager *GPUManager // Manages GPU data
|
||||
cache *systemDataCache // Cache for system stats based on cache time
|
||||
connectionManager *ConnectionManager // Channel to signal connection events
|
||||
@@ -164,6 +173,14 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
}
|
||||
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
|
||||
|
||||
// Include static info for requests with longer intervals (e.g., 15 min)
|
||||
// This reduces bandwidth by only sending static data occasionally
|
||||
if cacheTimeMs >= StaticInfoIntervalMs {
|
||||
staticInfoCopy := a.staticSystemInfo
|
||||
data.StaticInfo = &staticInfoCopy
|
||||
slog.Debug("Including static info", "cacheTimeMs", cacheTimeMs)
|
||||
}
|
||||
|
||||
if a.dockerManager != nil {
|
||||
if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
|
||||
data.Containers = containerStats
|
||||
@@ -225,7 +242,11 @@ func (a *Agent) getFingerprint() string {
|
||||
// if no fingerprint is found, generate one
|
||||
fingerprint, err := host.HostID()
|
||||
if err != nil || fingerprint == "" {
|
||||
fingerprint = a.systemInfo.Hostname + a.systemInfo.CpuModel
|
||||
cpuModel := ""
|
||||
if len(a.staticSystemInfo.Cpus) > 0 {
|
||||
cpuModel = a.staticSystemInfo.Cpus[0].Model
|
||||
}
|
||||
fingerprint = a.staticSystemInfo.Hostname + cpuModel
|
||||
}
|
||||
|
||||
// hash fingerprint
|
||||
|
||||
@@ -201,7 +201,7 @@ func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.R
|
||||
|
||||
if authRequest.NeedSysInfo {
|
||||
response.Name, _ = GetEnv("SYSTEM_NAME")
|
||||
response.Hostname = client.agent.systemInfo.Hostname
|
||||
response.Hostname = client.agent.staticSystemInfo.Hostname
|
||||
serverAddr := client.agent.connectionManager.serverOptions.Addr
|
||||
_, response.Port, _ = net.SplitHostPort(serverAddr)
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
|
||||
// If using podman, return client
|
||||
if strings.Contains(dockerHost, "podman") {
|
||||
a.systemInfo.Podman = true
|
||||
a.staticSystemInfo.Podman = true
|
||||
manager.goodDockerVersion = true
|
||||
return manager
|
||||
}
|
||||
|
||||
@@ -552,11 +552,8 @@ func createTestCombinedData() *system.CombinedData {
|
||||
},
|
||||
Info: system.Info{
|
||||
Hostname: "test-host",
|
||||
Cores: 8,
|
||||
CpuModel: "Test CPU Model",
|
||||
Uptime: 3600,
|
||||
AgentVersion: "0.12.0",
|
||||
Os: system.Linux,
|
||||
},
|
||||
Containers: []*container.Stats{
|
||||
{
|
||||
|
||||
254
agent/system.go
254
agent/system.go
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -12,7 +13,9 @@ import (
|
||||
"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"
|
||||
@@ -28,41 +31,76 @@ type prevDisk struct {
|
||||
|
||||
// Sets initial / non-changing values about the host system
|
||||
func (a *Agent) initializeSystemInfo() {
|
||||
a.systemInfo.AgentVersion = beszel.Version
|
||||
a.systemInfo.Hostname, _ = os.Hostname()
|
||||
hostname, _ := os.Hostname()
|
||||
a.staticSystemInfo.Hostname = hostname
|
||||
a.staticSystemInfo.AgentVersion = beszel.Version
|
||||
|
||||
platform, _, version, _ := host.PlatformInformation()
|
||||
platform, family, version, _ := host.PlatformInformation()
|
||||
|
||||
var osFamily, osVersion, osKernel string
|
||||
var osType system.Os
|
||||
if platform == "darwin" {
|
||||
a.systemInfo.KernelVersion = version
|
||||
a.systemInfo.Os = system.Darwin
|
||||
osKernel = version
|
||||
osFamily = "macOS" // macOS is the family name for Darwin
|
||||
osVersion = version
|
||||
} else if strings.Contains(platform, "indows") {
|
||||
a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
|
||||
a.systemInfo.Os = system.Windows
|
||||
osKernel = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
|
||||
osFamily = family
|
||||
osVersion = version
|
||||
osType = system.Windows
|
||||
} else if platform == "freebsd" {
|
||||
a.systemInfo.Os = system.Freebsd
|
||||
a.systemInfo.KernelVersion = version
|
||||
osKernel = version
|
||||
osFamily = family
|
||||
osVersion = version
|
||||
} else {
|
||||
a.systemInfo.Os = system.Linux
|
||||
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 a.systemInfo.KernelVersion == "" {
|
||||
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
||||
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 {
|
||||
a.systemInfo.CpuModel = info[0].ModelName
|
||||
}
|
||||
// cores / threads
|
||||
a.systemInfo.Cores, _ = cpu.Counts(false)
|
||||
if threads, err := cpu.Counts(true); err == nil {
|
||||
if threads > 0 && threads < a.systemInfo.Cores {
|
||||
// in lxc logical cores reflects container limits, so use that as cores if lower
|
||||
a.systemInfo.Cores = threads
|
||||
} else {
|
||||
a.systemInfo.Threads = threads
|
||||
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
|
||||
@@ -71,6 +109,41 @@ func (a *Agent) initializeSystemInfo() {
|
||||
} 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
|
||||
@@ -240,3 +313,136 @@ func getARCSize() (uint64, error) {
|
||||
|
||||
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,
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user