Files
ProxmoxDash/cpu.go

287 lines
6.7 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
const (
cpuInfoPath = "/proc/cpuinfo"
procStatPath = "/proc/stat"
)
type cpuCollector struct {
mu sync.Mutex
cpuInfoPath string
procStatPath string
static CPUStatic
previous cpuTimes
}
type CPUStatic struct {
Model string `json:"model"`
Sockets int `json:"sockets"`
PhysicalCores int `json:"physicalCores"`
LogicalCPUs int `json:"logicalCpus"`
}
type CPUMetrics struct {
CPUStatic
UsagePercent float64 `json:"usagePercent"`
FrequencyMHz float64 `json:"frequencyMhz"`
Load1 float64 `json:"load1"`
Load5 float64 `json:"load5"`
Load15 float64 `json:"load15"`
Uptime uint64 `json:"uptimeSeconds"`
Temperatures []Temperature `json:"temperatures"`
CollectedAt time.Time `json:"collectedAt"`
}
type Temperature struct {
Label string `json:"label"`
Celsius float64 `json:"celsius"`
}
type cpuTimes struct {
total uint64
idle uint64
}
func newCPUCollector(infoPath, statPath string) (*cpuCollector, error) {
static, err := readCPUStatic(infoPath)
if err != nil {
return nil, err
}
previous, err := readCPUTimes(statPath)
if err != nil {
return nil, err
}
return &cpuCollector{
cpuInfoPath: infoPath, procStatPath: statPath,
static: static, previous: previous,
}, nil
}
func (c *cpuCollector) collect() (CPUMetrics, error) {
c.mu.Lock()
defer c.mu.Unlock()
current, err := readCPUTimes(c.procStatPath)
if err != nil {
return CPUMetrics{}, err
}
var usage float64
totalDelta := current.total - c.previous.total
idleDelta := current.idle - c.previous.idle
if totalDelta > 0 {
usage = float64(totalDelta-idleDelta) / float64(totalDelta) * 100
}
c.previous = current
frequency, _ := averageFrequencyMHz(c.cpuInfoPath)
load1, load5, load15, _ := readLoadAverage()
uptime, _ := readUptime()
return CPUMetrics{
CPUStatic: c.static, UsagePercent: usage, FrequencyMHz: frequency,
Load1: load1, Load5: load5, Load15: load15, Uptime: uptime,
Temperatures: readTemperatures(), CollectedAt: time.Now(),
}, nil
}
func readCPUStatic(path string) (CPUStatic, error) {
file, err := os.Open(path)
if err != nil {
return CPUStatic{}, err
}
defer file.Close()
var model string
logical := 0
physicalIDs := map[string]struct{}{}
cores := map[string]struct{}{}
physicalID := "0"
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
physicalID = "0"
continue
}
key, value, found := strings.Cut(line, ":")
if !found {
continue
}
key, value = strings.TrimSpace(key), strings.TrimSpace(value)
switch key {
case "processor":
logical++
case "model name", "Hardware":
if model == "" {
model = value
}
case "physical id":
physicalID = value
physicalIDs[value] = struct{}{}
case "core id":
cores[physicalID+":"+value] = struct{}{}
}
}
if err := scanner.Err(); err != nil {
return CPUStatic{}, err
}
if model == "" {
return CPUStatic{}, fmt.Errorf("модель CPU не найдена в %s", path)
}
if len(physicalIDs) == 0 {
physicalIDs["0"] = struct{}{}
}
physicalCores := len(cores)
if physicalCores == 0 {
physicalCores = logical
}
return CPUStatic{
Model: model, Sockets: len(physicalIDs),
PhysicalCores: physicalCores, LogicalCPUs: logical,
}, nil
}
func cpuModel(path string) (string, error) {
info, err := readCPUStatic(path)
return info.Model, err
}
func readCPUTimes(path string) (cpuTimes, error) {
file, err := os.Open(path)
if err != nil {
return cpuTimes{}, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
if !scanner.Scan() {
return cpuTimes{}, fmt.Errorf("пустой файл %s", path)
}
fields := strings.Fields(scanner.Text())
if len(fields) < 5 || fields[0] != "cpu" {
return cpuTimes{}, fmt.Errorf("неожиданный формат %s", path)
}
var values []uint64
for _, field := range fields[1:] {
value, err := strconv.ParseUint(field, 10, 64)
if err != nil {
return cpuTimes{}, err
}
values = append(values, value)
}
var total uint64
for _, value := range values {
total += value
}
idle := values[3]
if len(values) > 4 {
idle += values[4]
}
return cpuTimes{total: total, idle: idle}, nil
}
func averageFrequencyMHz(path string) (float64, error) {
file, err := os.Open(path)
if err != nil {
return 0, err
}
defer file.Close()
var sum float64
var count int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
key, value, found := strings.Cut(scanner.Text(), ":")
if found && strings.TrimSpace(key) == "cpu MHz" {
mhz, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
if err == nil {
sum += mhz
count++
}
}
}
if count == 0 {
return 0, fmt.Errorf("частота CPU недоступна")
}
return sum / float64(count), scanner.Err()
}
func readLoadAverage() (float64, float64, float64, error) {
data, err := os.ReadFile("/proc/loadavg")
if err != nil {
return 0, 0, 0, err
}
fields := strings.Fields(string(data))
if len(fields) < 3 {
return 0, 0, 0, fmt.Errorf("неожиданный формат /proc/loadavg")
}
one, err1 := strconv.ParseFloat(fields[0], 64)
five, err2 := strconv.ParseFloat(fields[1], 64)
fifteen, err3 := strconv.ParseFloat(fields[2], 64)
if err1 != nil || err2 != nil || err3 != nil {
return 0, 0, 0, fmt.Errorf("не удалось прочитать load average")
}
return one, five, fifteen, nil
}
func readUptime() (uint64, error) {
data, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0, err
}
seconds, err := strconv.ParseFloat(strings.Fields(string(data))[0], 64)
return uint64(seconds), err
}
func readTemperatures() []Temperature {
var result []Temperature
paths, _ := filepath.Glob("/sys/class/hwmon/hwmon*/temp*_input")
for _, path := range paths {
hwmonDir := filepath.Dir(path)
chipData, _ := os.ReadFile(filepath.Join(hwmonDir, "name"))
chip := strings.TrimSpace(string(chipData))
if !isCPUTemperatureChip(chip) {
continue
}
data, err := os.ReadFile(path)
if err != nil {
continue
}
raw, err := strconv.ParseFloat(strings.TrimSpace(string(data)), 64)
if err != nil || raw <= 0 {
continue
}
label := filepath.Base(path)
labelPath := strings.TrimSuffix(path, "_input") + "_label"
if data, err := os.ReadFile(labelPath); err == nil {
label = strings.TrimSpace(string(data))
}
if chip != "" {
label = chip + " · " + label
}
result = append(result, Temperature{Label: label, Celsius: raw / 1000})
}
return result
}
func isCPUTemperatureChip(name string) bool {
switch name {
case "coretemp", "k10temp", "zenpower", "cpu_thermal", "soc_thermal":
return true
default:
return false
}
}