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 } }