287 lines
6.7 KiB
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
|
|
}
|
|
}
|