Files
deploy-felhom-compose/controller/internal/system/cpu_linux.go
T
admin 95c821deb2 feat: comprehensive debug logging across all controller modules
Add detailed [DEBUG] logging to every controller module when
logging.level is set to "debug". Each module with stateful debug
uses SetDebug(bool) wired from main.go. Covers stacks, backup,
cloudflare, integrations, system, monitor, settings, scheduler,
web handlers, storage, metrics, API, selfupdate, and assets.

Also includes the app export/import (.fab bundles) feature from
v0.32.0 and its debug page integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:14:43 +01:00

143 lines
3.1 KiB
Go

//go:build linux
package system
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"sync"
"time"
)
// CPUCollector samples CPU usage in the background by reading /proc/stat.
type CPUCollector struct {
mu sync.RWMutex
cpuPercent float64
sampleRate time.Duration
cancel context.CancelFunc
}
// NewCPUCollector creates a new CPU collector with the given sample rate.
func NewCPUCollector(sampleRate time.Duration) *CPUCollector {
return &CPUCollector{
sampleRate: sampleRate,
}
}
// Start begins background CPU sampling.
func (c *CPUCollector) Start(ctx context.Context) {
ctx, c.cancel = context.WithCancel(ctx)
debugf("[DEBUG] [system] CPUCollector.Start: sampleRate=%s", c.sampleRate)
go c.loop(ctx)
}
// Stop stops the background CPU sampling.
func (c *CPUCollector) Stop() {
if c.cancel != nil {
c.cancel()
}
}
// CPUPercent returns the latest CPU usage percentage (0-100).
func (c *CPUCollector) CPUPercent() float64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cpuPercent
}
func (c *CPUCollector) loop(ctx context.Context) {
firstSample := true
for {
// Read first sample
idle1, total1, err := readCPUStat()
if err != nil {
debugf("[DEBUG] [system] CPUCollector: readCPUStat error: %v", err)
select {
case <-ctx.Done():
return
case <-time.After(c.sampleRate):
continue
}
}
// Wait for sample interval
select {
case <-ctx.Done():
return
case <-time.After(c.sampleRate):
}
// Read second sample
idle2, total2, err := readCPUStat()
if err != nil {
continue
}
totalDelta := total2 - total1
idleDelta := idle2 - idle1
if totalDelta > 0 {
busyDelta := totalDelta - idleDelta
percent := float64(busyDelta) / float64(totalDelta) * 100
c.mu.Lock()
c.cpuPercent = percent
c.mu.Unlock()
if firstSample {
debugf("[DEBUG] [system] CPUCollector: first sample — cpu=%.1f%% (idle=%d total=%d)", percent, idleDelta, totalDelta)
firstSample = false
}
}
}
}
// readCPUStat reads /proc/stat and returns idle and total CPU jiffies.
// First line format: cpu <user> <nice> <system> <idle> <iowait> <irq> <softirq> <steal>
func readCPUStat() (idle, total uint64, err error) {
f, err := os.Open("/proc/stat")
if err != nil {
return 0, 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return 0, 0, fmt.Errorf("empty /proc/stat")
}
line := scanner.Text()
if !strings.HasPrefix(line, "cpu ") {
return 0, 0, fmt.Errorf("unexpected /proc/stat first line: %s", line)
}
fields := strings.Fields(line)
if len(fields) < 9 {
return 0, 0, fmt.Errorf("/proc/stat has too few fields: %d", len(fields))
}
// Fields: cpu user(1) nice(2) system(3) idle(4) iowait(5) irq(6) softirq(7) steal(8)
var values [8]uint64
for i := 0; i < 8; i++ {
var v uint64
for _, c := range fields[i+1] {
if c >= '0' && c <= '9' {
v = v*10 + uint64(c-'0')
}
}
values[i] = v
}
// idle_total = idle + iowait
idleTotal := values[3] + values[4]
// total = sum of all
var totalVal uint64
for _, v := range values {
totalVal += v
}
return idleTotal, totalVal, nil
}