mirror of
https://github.com/Xevion/glance.git
synced 2025-12-10 06:07:20 -06:00
Add server-stats widget
This commit is contained in:
252
pkg/sysinfo/sysinfo.go
Normal file
252
pkg/sysinfo/sysinfo.go
Normal file
@@ -0,0 +1,252 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user