95c821deb2
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>
143 lines
3.1 KiB
Go
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
|
|
}
|