feat: resource_pressure - disk free, CPU freq/throttle/temp, GPU temp via nvidia-smi
This commit is contained in:
108
agent/client/resource_unix.go
Normal file
108
agent/client/resource_unix.go
Normal file
@@ -0,0 +1,108 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// collectResourcePressure gathers disk, CPU frequency/temp, and GPU metrics on Linux/macOS.
|
||||
func collectResourcePressure() *ResourcePressure {
|
||||
r := &ResourcePressure{}
|
||||
|
||||
// ── Disk free space ───────────────────────────────────────────────────────
|
||||
exe, _ := os.Executable()
|
||||
if exe == "" {
|
||||
exe = "/"
|
||||
}
|
||||
freeGB, totalGB, freePct := unixDiskStats(filepath.Dir(exe))
|
||||
if freePct >= 0 {
|
||||
r.DiskFreeGB = &freeGB
|
||||
r.DiskTotalGB = &totalGB
|
||||
r.DiskFreePct = &freePct
|
||||
}
|
||||
|
||||
// ── CPU frequency ─────────────────────────────────────────────────────────
|
||||
cur := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
|
||||
max := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq")
|
||||
if cur > 0 {
|
||||
mhz := cur / 1000
|
||||
r.CPUFreqMHz = &mhz
|
||||
}
|
||||
if max > 0 {
|
||||
mhz := max / 1000
|
||||
r.CPUMaxMHz = &mhz
|
||||
if cur > 0 {
|
||||
throttled := float64(cur) < float64(max)*0.80
|
||||
r.CPUThrottle = &throttled
|
||||
}
|
||||
}
|
||||
|
||||
// ── CPU thermal ───────────────────────────────────────────────────────────
|
||||
if temp := probeCPUTemp(); temp > 0 {
|
||||
r.CPUTempC = &temp
|
||||
}
|
||||
|
||||
// ── GPU (NVIDIA) ──────────────────────────────────────────────────────────
|
||||
r.GPUTempC, r.GPUUsagePct = probeGPU()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// unixDiskStats returns (freeGB, totalGB, freePct) for the filesystem at path.
|
||||
func unixDiskStats(path string) (freeGB, totalGB float64, freePct int) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return 0, 0, -1
|
||||
}
|
||||
if stat.Blocks == 0 {
|
||||
return 0, 0, -1
|
||||
}
|
||||
bsize := uint64(stat.Bsize)
|
||||
total := stat.Blocks * bsize
|
||||
free := stat.Bavail * bsize
|
||||
|
||||
const gb = 1024 * 1024 * 1024
|
||||
totalGB = math.Round(float64(total)/gb*100) / 100
|
||||
freeGB = math.Round(float64(free)/gb*100) / 100
|
||||
freePct = int(float64(free) / float64(total) * 100)
|
||||
return
|
||||
}
|
||||
|
||||
// probeCPUTemp reads from the thermal subsystem. Returns 0 when unavailable.
|
||||
func probeCPUTemp() int {
|
||||
// Try coretemp / k10temp style entries first (millidegrees)
|
||||
for _, zone := range []string{
|
||||
"/sys/class/thermal/thermal_zone0/temp",
|
||||
"/sys/class/thermal/thermal_zone1/temp",
|
||||
"/sys/class/hwmon/hwmon0/temp1_input",
|
||||
"/sys/class/hwmon/hwmon1/temp1_input",
|
||||
} {
|
||||
if n := readSysInt(zone); n > 0 {
|
||||
// values > 1000 are in millidegrees
|
||||
if n > 1000 {
|
||||
return n / 1000
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// readSysInt reads a single integer from a sysfs/procfs file.
|
||||
func readSysInt(path string) int {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user