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>
This commit is contained in:
@@ -30,6 +30,7 @@ func NewCPUCollector(sampleRate time.Duration) *CPUCollector {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -48,10 +49,12 @@ func (c *CPUCollector) CPUPercent() float64 {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -82,6 +85,11 @@ func (c *CPUCollector) loop(ctx context.Context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
package system
|
||||
|
||||
import "log"
|
||||
|
||||
// DebugLogger, when non-nil, enables debug-level logging for the system package.
|
||||
// Set this to the application's *log.Logger when config logging.level == "debug".
|
||||
// When nil, no debug output is emitted.
|
||||
var DebugLogger *log.Logger
|
||||
|
||||
// debugf logs a formatted message if DebugLogger is set.
|
||||
func debugf(format string, args ...any) {
|
||||
if DebugLogger != nil {
|
||||
DebugLogger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// SystemInfo holds system resource usage information.
|
||||
type SystemInfo struct {
|
||||
TotalMemMB uint64 `json:"total_mem_mb"`
|
||||
|
||||
@@ -10,12 +10,16 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetInfo reads system memory, disk, CPU, load, and temperature info.
|
||||
// hddPath is the mount path for external HDD; if empty, HDD info is skipped.
|
||||
// cpuCollector provides the latest CPU usage sample; may be nil.
|
||||
func GetInfo(hddPath string, cpuCollector *CPUCollector) SystemInfo {
|
||||
start := time.Now()
|
||||
debugf("[DEBUG] [system] GetInfo starting (hddPath=%q, hasCPUCollector=%v)", hddPath, cpuCollector != nil)
|
||||
|
||||
info := SystemInfo{}
|
||||
|
||||
// --- Memory from /proc/meminfo ---
|
||||
@@ -41,6 +45,15 @@ func GetInfo(hddPath string, cpuCollector *CPUCollector) SystemInfo {
|
||||
info.CPUPercent = cpuCollector.CPUPercent()
|
||||
}
|
||||
|
||||
debugf("[DEBUG] [system] GetInfo done in %s — mem=%dMB/%dMB (%.1f%%), rootDisk=%.1fGB/%.1fGB (%.1f%%), load=%.2f/%.2f/%.2f, temp=%.1f°C (%s), cpu=%.1f%%",
|
||||
time.Since(start).Round(time.Millisecond),
|
||||
info.UsedMemMB, info.TotalMemMB, info.MemPercent,
|
||||
info.DiskUsedGB, info.DiskTotalGB, info.DiskPercent,
|
||||
info.LoadAvg1, info.LoadAvg5, info.LoadAvg15,
|
||||
info.TemperatureCelsius, info.TemperatureSource,
|
||||
info.CPUPercent,
|
||||
)
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -67,6 +80,7 @@ func GetMemoryMB() (totalMB, usedMB int, err error) {
|
||||
func readMemInfo(info *SystemInfo) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
debugf("[DEBUG] [system] readMemInfo: failed to open /proc/meminfo: %v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
@@ -91,6 +105,10 @@ func readMemInfo(info *SystemInfo) {
|
||||
info.AvailMemMB = availKB / 1024
|
||||
info.UsedMemMB = info.TotalMemMB - info.AvailMemMB
|
||||
info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100
|
||||
debugf("[DEBUG] [system] readMemInfo: totalKB=%d availKB=%d → total=%dMB avail=%dMB used=%dMB (%.1f%%)",
|
||||
totalKB, availKB, info.TotalMemMB, info.AvailMemMB, info.UsedMemMB, info.MemPercent)
|
||||
} else {
|
||||
debugf("[DEBUG] [system] readMemInfo: could not parse MemTotal from /proc/meminfo")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +134,7 @@ func parseMemLine(line string) uint64 {
|
||||
func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *float64) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
debugf("[DEBUG] [system] readDiskUsage: statfs(%q) failed: %v", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -131,15 +150,20 @@ func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *floa
|
||||
if total > 0 {
|
||||
*percent = float64(used) / float64(total) * 100
|
||||
}
|
||||
debugf("[DEBUG] [system] readDiskUsage: path=%q bsize=%d total=%.1fGB used=%.1fGB avail=%.1fGB (%.1f%%)",
|
||||
path, bsize, *totalGB, *usedGB, *availGB, *percent)
|
||||
}
|
||||
|
||||
// readLoadAvg reads 1/5/15 minute load averages from /proc/loadavg.
|
||||
func readLoadAvg(info *SystemInfo) {
|
||||
data, err := os.ReadFile("/proc/loadavg")
|
||||
if err != nil {
|
||||
debugf("[DEBUG] [system] readLoadAvg: failed to read /proc/loadavg: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Sscanf(string(data), "%f %f %f", &info.LoadAvg1, &info.LoadAvg5, &info.LoadAvg15)
|
||||
debugf("[DEBUG] [system] readLoadAvg: raw=%q → 1m=%.2f 5m=%.2f 15m=%.2f",
|
||||
strings.TrimSpace(string(data)), info.LoadAvg1, info.LoadAvg5, info.LoadAvg15)
|
||||
}
|
||||
|
||||
// readTemperature reads CPU/SoC temperature from thermal zones.
|
||||
@@ -149,6 +173,7 @@ func readTemperature(info *SystemInfo) {
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if readThermalZones(prefix, info) {
|
||||
debugf("[DEBUG] [system] readTemperature: found via thermal_zone at %s — %.1f°C (%s)", prefix, info.TemperatureCelsius, info.TemperatureSource)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -156,9 +181,12 @@ func readTemperature(info *SystemInfo) {
|
||||
// Fallback: try hwmon
|
||||
for _, prefix := range prefixes {
|
||||
if readHwmon(prefix, info) {
|
||||
debugf("[DEBUG] [system] readTemperature: found via hwmon at %s — %.1f°C (%s)", prefix, info.TemperatureCelsius, info.TemperatureSource)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
debugf("[DEBUG] [system] readTemperature: no temperature source found")
|
||||
}
|
||||
|
||||
func readThermalZones(sysPrefix string, info *SystemInfo) bool {
|
||||
@@ -169,6 +197,7 @@ func readThermalZones(sysPrefix string, info *SystemInfo) bool {
|
||||
}
|
||||
|
||||
sort.Strings(matches)
|
||||
debugf("[DEBUG] [system] readThermalZones: %s — found %d zones", sysPrefix, len(matches))
|
||||
|
||||
var maxTemp float64
|
||||
var maxSource string
|
||||
|
||||
@@ -65,6 +65,7 @@ type DiskUsageInfo struct {
|
||||
func GetDiskUsage(path string) *DiskUsageInfo {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
debugf("[DEBUG] [system] GetDiskUsage: statfs(%q) failed: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,6 +85,8 @@ func GetDiskUsage(path string) *DiskUsageInfo {
|
||||
}
|
||||
info.TotalHuman = formatGB(info.TotalGB)
|
||||
info.UsedHuman = formatGB(info.UsedGB)
|
||||
debugf("[DEBUG] [system] GetDiskUsage: path=%q total=%s used=%s avail=%.1fGB (%.1f%%)",
|
||||
path, info.TotalHuman, info.UsedHuman, info.AvailGB, info.UsedPercent)
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -105,10 +108,12 @@ type FSInfo struct {
|
||||
func GetFSInfo(path string) *FSInfo {
|
||||
out, err := exec.Command("findmnt", "-n", "-o", "SOURCE,FSTYPE", "--target", path).Output()
|
||||
if err != nil {
|
||||
debugf("[DEBUG] [system] GetFSInfo: findmnt(%q) failed: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
fields := strings.Fields(strings.TrimSpace(string(out)))
|
||||
if len(fields) < 2 {
|
||||
debugf("[DEBUG] [system] GetFSInfo: findmnt(%q) returned unexpected output: %q", path, strings.TrimSpace(string(out)))
|
||||
return nil
|
||||
}
|
||||
info := &FSInfo{
|
||||
@@ -117,6 +122,7 @@ func GetFSInfo(path string) *FSInfo {
|
||||
}
|
||||
// Try to get disk model from sysfs
|
||||
info.Model = diskModel(info.Device)
|
||||
debugf("[DEBUG] [system] GetFSInfo: path=%q device=%s fstype=%s model=%q", path, info.Device, info.FSType, info.Model)
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -136,6 +142,7 @@ type DestinationHealth struct {
|
||||
// CheckBackupDestination performs tiered validation of a cross-drive backup destination.
|
||||
// Returns a DestinationHealth describing any issues found.
|
||||
func CheckBackupDestination(path string) DestinationHealth {
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q", path)
|
||||
h := DestinationHealth{Severity: "ok"}
|
||||
|
||||
// Tier 1: path must exist
|
||||
@@ -143,6 +150,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = "A cél tárhely (" + path + ") nem létezik!"
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier1 FAIL (not exists)", path)
|
||||
return h
|
||||
}
|
||||
h.Exists = true
|
||||
@@ -152,6 +160,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
h.Warning = "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat."
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier2 FAIL (not writable)", path)
|
||||
return h
|
||||
}
|
||||
h.Writable = true
|
||||
@@ -165,9 +174,11 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
"Meghajtóhiba esetén az eredeti adat és a mentés is elveszhet. " +
|
||||
"Külső meghajtó használata javasolt."
|
||||
h.Severity = "warning"
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier3 WARN (same block device as /)", path)
|
||||
// Don't return early — also check disk usage
|
||||
} else {
|
||||
h.MountPoint = true
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier3 OK (different block device)", path)
|
||||
}
|
||||
|
||||
// Tier 4: disk usage checks
|
||||
@@ -199,6 +210,8 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
}
|
||||
}
|
||||
|
||||
debugf("[DEBUG] [system] CheckBackupDestination: path=%q — result: severity=%s blocked=%v freeGB=%.1f usedPct=%.1f%%",
|
||||
path, h.Severity, h.Blocked, h.FreeGB, h.UsedPercent)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -256,8 +269,11 @@ type ProbeResult struct {
|
||||
// ProbeStoragePath checks if a storage path is responsive.
|
||||
// Uses a goroutine with a 3-second timeout to avoid blocking on dead mounts.
|
||||
func ProbeStoragePath(path string) ProbeResult {
|
||||
start := time.Now()
|
||||
|
||||
// Quick check: does the path exist at all?
|
||||
if _, err := os.Lstat(path); os.IsNotExist(err) {
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — not exists (%s)", path, time.Since(start).Round(time.Millisecond))
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: err}
|
||||
}
|
||||
|
||||
@@ -273,17 +289,22 @@ func ProbeStoragePath(path string) ProbeResult {
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
elapsed := time.Since(start).Round(time.Millisecond)
|
||||
if res.err == nil {
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — connected (%s)", path, elapsed)
|
||||
return ProbeResult{Status: ProbeConnected}
|
||||
}
|
||||
errStr := res.err.Error()
|
||||
if strings.Contains(errStr, "transport endpoint") ||
|
||||
strings.Contains(errStr, "input/output error") ||
|
||||
strings.Contains(errStr, "no such device") {
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected: %v (%s)", path, res.err, elapsed)
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: res.err}
|
||||
}
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected (other error): %v (%s)", path, res.err, elapsed)
|
||||
return ProbeResult{Status: ProbeDisconnected, Err: res.err}
|
||||
case <-time.After(3 * time.Second):
|
||||
debugf("[DEBUG] [system] ProbeStoragePath: path=%q — TIMEOUT (3s)", path)
|
||||
return ProbeResult{Status: ProbeTimeout, Err: fmt.Errorf("stat timed out after 3s")}
|
||||
}
|
||||
}
|
||||
@@ -302,11 +323,11 @@ func IsUSBDevice(devicePath string) bool {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(link, "/usb") {
|
||||
return true
|
||||
}
|
||||
return false // found the sysfs entry, but not USB
|
||||
isUSB := strings.Contains(link, "/usb")
|
||||
debugf("[DEBUG] [system] IsUSBDevice: device=%q disk=%q sysfs=%s → usb=%v", devicePath, disk, link, isUSB)
|
||||
return isUSB
|
||||
}
|
||||
debugf("[DEBUG] [system] IsUSBDevice: device=%q disk=%q — no sysfs entry found", devicePath, disk)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user