Files
xboard-node/internal/monitor/monitor.go
T

173 lines
4.2 KiB
Go

package monitor
import (
"runtime"
"strings"
"sync"
"time"
"github.com/cedar2025/xboard-node/internal/nlog"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
)
var startTime = time.Now()
func init() {
// Warm up the CPU sampler. The first cpu.Percent call with interval=0
// always returns 0% because it has no prior sample. This throwaway call
// seeds the baseline so subsequent Collect() calls return real values.
cpu.Percent(500*time.Millisecond, false)
// Seed the network baseline so the first Collect() can compute rates.
collectNetSpeed()
}
// Status holds system resource metrics
type Status struct {
Uptime uint64
CPU float64
CPUPerCore []float64
Load1 float64
Load5 float64
Load15 float64
MemTotal uint64
MemUsed uint64
SwapTotal uint64
SwapUsed uint64
DiskTotal uint64
DiskUsed uint64
Goroutines int
// Network speed (bytes/sec), -1 means unavailable (first sample).
NetInSpeed float64
NetOutSpeed float64
// GC metrics (process-wide)
NumGC uint32
LastPauseMS float64
}
// netBaseline tracks the previous network counters for rate calculation.
var (
netMu sync.Mutex
netPrevRecv uint64
netPrevSent uint64
netPrevTime time.Time
netHasBase bool
)
// skipInterface returns true for loopback and common virtual interfaces.
func skipInterface(name string) bool {
lower := strings.ToLower(name)
for _, prefix := range []string{"lo", "docker", "veth", "br-", "virbr", "vnet", "tun", "tap"} {
if strings.HasPrefix(lower, prefix) {
return true
}
}
return false
}
// collectNetSpeed calculates network in/out bytes per second since last call.
// Returns -1, -1 on first call or if counters decreased (reboot).
func collectNetSpeed() (inSpeed, outSpeed float64) {
counters, err := net.IOCounters(true) // per-interface
if err != nil {
nlog.Core().Debug("failed to get network counters", "error", err)
return -1, -1
}
var totalRecv, totalSent uint64
for _, c := range counters {
if skipInterface(c.Name) {
continue
}
totalRecv += c.BytesRecv
totalSent += c.BytesSent
}
now := time.Now()
netMu.Lock()
defer netMu.Unlock()
if !netHasBase {
netPrevRecv, netPrevSent, netPrevTime, netHasBase = totalRecv, totalSent, now, true
return -1, -1
}
elapsed := now.Sub(netPrevTime).Seconds()
if elapsed <= 0 {
return -1, -1
}
// Counter decreased → system reboot or interface reset; reset baseline.
if totalRecv < netPrevRecv || totalSent < netPrevSent {
netPrevRecv, netPrevSent, netPrevTime = totalRecv, totalSent, now
return -1, -1
}
inSpeed = float64(totalRecv-netPrevRecv) / elapsed
outSpeed = float64(totalSent-netPrevSent) / elapsed
netPrevRecv, netPrevSent, netPrevTime = totalRecv, totalSent, now
return inSpeed, outSpeed
}
// Collect gathers current system metrics
func Collect() Status {
var s Status
s.Uptime = uint64(time.Since(startTime).Seconds())
if cpuPercent, err := cpu.Percent(0, false); err == nil && len(cpuPercent) > 0 {
s.CPU = cpuPercent[0]
} else if err != nil {
nlog.Core().Debug("failed to get CPU usage", "error", err)
}
// Per-core CPU usage (best-effort; safe if it fails).
if perCore, err := cpu.Percent(0, true); err == nil && len(perCore) > 0 {
s.CPUPerCore = perCore
}
if loadAvg, err := load.Avg(); err == nil {
s.Load1 = loadAvg.Load1
s.Load5 = loadAvg.Load5
s.Load15 = loadAvg.Load15
}
if vmStat, err := mem.VirtualMemory(); err == nil {
s.MemTotal = vmStat.Total
s.MemUsed = vmStat.Used
}
if swapStat, err := mem.SwapMemory(); err == nil {
s.SwapTotal = swapStat.Total
s.SwapUsed = swapStat.Used
}
if diskStat, err := disk.Usage("/"); err == nil {
s.DiskTotal = diskStat.Total
s.DiskUsed = diskStat.Used
}
s.NetInSpeed, s.NetOutSpeed = collectNetSpeed()
// GC metrics
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
s.Goroutines = runtime.NumGoroutine()
s.NumGC = ms.NumGC
if ms.NumGC > 0 {
// PauseNs is a ring buffer of the most recent GC pause times.
idx := (ms.NumGC - 1) % uint32(len(ms.PauseNs))
s.LastPauseMS = float64(ms.PauseNs[idx]) / 1e6
}
return s
}