252 lines
7.2 KiB
Go
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
|
|
}
|