glance/pkg/sysinfo/sysinfo.go
2025-02-09 17:52:22 +00:00

252 lines
7.2 KiB
Go

package sysinfo
import (
"fmt"
"math"
"runtime"
"sort"
"strconv"
"time"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/sensors"
)
type timestampJSON struct {
time.Time
}
func (t timestampJSON) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(t.Unix(), 10)), nil
}
func (t *timestampJSON) UnmarshalJSON(data []byte) error {
i, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
t.Time = time.Unix(i, 0)
return nil
}
type SystemInfo struct {
HostInfoIsAvailable bool `json:"host_info_is_available"`
BootTime timestampJSON `json:"boot_time"`
Hostname string `json:"hostname"`
Platform string `json:"platform"`
CPU struct {
LoadIsAvailable bool `json:"load_is_available"`
Load1Percent uint8 `json:"load1_percent"`
Load15Percent uint8 `json:"load15_percent"`
TemperatureIsAvailable bool `json:"temperature_is_available"`
TemperatureC uint8 `json:"temperature_c"`
} `json:"cpu"`
Memory struct {
IsAvailable bool `json:"memory_is_available"`
TotalMB uint64 `json:"total_mb"`
UsedMB uint64 `json:"used_mb"`
UsedPercent uint8 `json:"used_percent"`
SwapIsAvailable bool `json:"swap_is_available"`
SwapTotalMB uint64 `json:"swap_total_mb"`
SwapUsedMB uint64 `json:"swap_used_mb"`
SwapUsedPercent uint8 `json:"swap_used_percent"`
} `json:"memory"`
Mountpoints []MountpointInfo `json:"mountpoints"`
}
type MountpointInfo struct {
Path string `json:"path"`
Name string `json:"name"`
TotalMB uint64 `json:"total_mb"`
UsedMB uint64 `json:"used_mb"`
UsedPercent uint8 `json:"used_percent"`
}
type SystemInfoRequest struct {
CPUTempSensor string `yaml:"cpu-temp-sensor"`
Mountpoints map[string]MointpointRequest `yaml:"mountpoints"`
}
type MointpointRequest struct {
Name string `yaml:"name"`
Hide bool `yaml:"hide"`
}
// Currently caches hostname indefinitely which isn't ideal
// Potential issue with caching boot time as it may not initially get reported correctly:
// https://github.com/shirou/gopsutil/issues/842#issuecomment-1908972344
var cachedHostInfo = struct {
available bool
hostname string
platform string
bootTime timestampJSON
}{}
func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
if req == nil {
req = &SystemInfoRequest{}
}
var errs []error
addErr := func(err error) {
errs = append(errs, err)
}
info := &SystemInfo{
Mountpoints: []MountpointInfo{},
}
applyCachedHostInfo := func() {
info.HostInfoIsAvailable = true
info.BootTime = cachedHostInfo.bootTime
info.Hostname = cachedHostInfo.hostname
info.Platform = cachedHostInfo.platform
}
if cachedHostInfo.available {
applyCachedHostInfo()
} else {
hostInfo, err := host.Info()
if err == nil {
cachedHostInfo.available = true
cachedHostInfo.bootTime = timestampJSON{time.Unix(int64(hostInfo.BootTime), 0)}
cachedHostInfo.hostname = hostInfo.Hostname
cachedHostInfo.platform = hostInfo.Platform
applyCachedHostInfo()
} else {
addErr(fmt.Errorf("getting host info: %v", err))
}
}
coreCount, err := cpu.Counts(true)
if err == nil {
loadAvg, err := load.Avg()
if err == nil {
info.CPU.LoadIsAvailable = true
if runtime.GOOS == "windows" {
// The numbers returned here seem unreliable on Windows. Even with the CPU pegged
// at close to 50% for multiple minutes, load1 is sometimes way under or way over
// with no clear pattern. Dividing by core count gives numbers that are way too
// low so that's likely not necessary as it is with unix.
info.CPU.Load1Percent = uint8(math.Min(loadAvg.Load1*100, 100))
info.CPU.Load15Percent = uint8(math.Min(loadAvg.Load15*100, 100))
} else {
info.CPU.Load1Percent = uint8(math.Min((loadAvg.Load1/float64(coreCount))*100, 100))
info.CPU.Load15Percent = uint8(math.Min((loadAvg.Load15/float64(coreCount))*100, 100))
}
} else {
addErr(fmt.Errorf("getting load avg: %v", err))
}
} else {
addErr(fmt.Errorf("getting core count: %v", err))
}
memory, err := mem.VirtualMemory()
if err == nil {
info.Memory.IsAvailable = true
info.Memory.TotalMB = memory.Total / 1024 / 1024
info.Memory.UsedMB = memory.Used / 1024 / 1024
info.Memory.UsedPercent = uint8(math.Min(memory.UsedPercent, 100))
} else {
addErr(fmt.Errorf("getting memory info: %v", err))
}
swapMemory, err := mem.SwapMemory()
if err == nil {
info.Memory.SwapIsAvailable = true
info.Memory.SwapTotalMB = swapMemory.Total / 1024 / 1024
info.Memory.SwapUsedMB = swapMemory.Used / 1024 / 1024
info.Memory.SwapUsedPercent = uint8(math.Min(swapMemory.UsedPercent, 100))
} else {
addErr(fmt.Errorf("getting swap memory info: %v", err))
}
// currently disabled on Windows because it requires elevated privilidges, otherwise
// keeps returning a single sensor with key "ACPI\\ThermalZone\\TZ00_0" which
// doesn't seem to be the CPU sensor or correspond to anything useful when
// compared against the temperatures Libre Hardware Monitor reports
if runtime.GOOS != "windows" {
sensorReadings, err := sensors.SensorsTemperatures()
if err == nil {
if req.CPUTempSensor != "" {
for i := range sensorReadings {
if sensorReadings[i].SensorKey == req.CPUTempSensor {
info.CPU.TemperatureIsAvailable = true
info.CPU.TemperatureC = uint8(sensorReadings[i].Temperature)
break
}
}
if !info.CPU.TemperatureIsAvailable {
addErr(fmt.Errorf("CPU temperature sensor %s not found", req.CPUTempSensor))
}
} else if cpuTempSensor := inferCPUTempSensor(sensorReadings); cpuTempSensor != nil {
info.CPU.TemperatureIsAvailable = true
info.CPU.TemperatureC = uint8(cpuTempSensor.Temperature)
}
} else {
addErr(fmt.Errorf("getting sensor readings: %v", err))
}
}
filesystems, err := disk.Partitions(false)
if err == nil {
for _, fs := range filesystems {
mpReq, ok := req.Mountpoints[fs.Mountpoint]
if ok && mpReq.Hide {
continue
}
usage, err := disk.Usage(fs.Mountpoint)
if err == nil {
mpInfo := MountpointInfo{
Path: fs.Mountpoint,
Name: mpReq.Name,
TotalMB: usage.Total / 1024 / 1024,
UsedMB: usage.Used / 1024 / 1024,
UsedPercent: uint8(math.Min(usage.UsedPercent, 100)),
}
info.Mountpoints = append(info.Mountpoints, mpInfo)
} else {
addErr(fmt.Errorf("getting filesystem usage for %s: %v", fs.Mountpoint, err))
}
}
} else {
addErr(fmt.Errorf("getting filesystems: %v", err))
}
sort.Slice(info.Mountpoints, func(a, b int) bool {
return info.Mountpoints[a].UsedPercent > info.Mountpoints[b].UsedPercent
})
return info, errs
}
func inferCPUTempSensor(sensors []sensors.TemperatureStat) *sensors.TemperatureStat {
for i := range sensors {
switch sensors[i].SensorKey {
case
"coretemp_package_id_0", // intel / linux
"coretemp", // intel / linux
"k10temp", // amd / linux
"zenpower", // amd / linux
"cpu_thermal": // raspberry pi / linux
return &sensors[i]
}
}
return nil
}