173 lines
4.2 KiB
Go
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
|
|
}
|