Files
netbird/client/ui/guilog/debuglog.go
Zoltán Papp ff6aef5e2a feat(ui): GUI debug logging follows daemon log level + debug bundle
When the daemon is set to debug/trace, the GUI now automatically writes a
rotated gui-client.log in the user's config dir and the daemon's debug bundle
collects it. The UI learns the level both at startup (daemon already in debug)
and live, by piggybacking the existing SubscribeEvents stream: the daemon
publishes a marked log-level-changed SystemEvent (and a per-subscription
snapshot), which DaemonFeed routes to guilog.DebugLog instead of an OS toast.
The UI registers its log path via a new RegisterUILog RPC so the root daemon,
which can't resolve the user's config dir, knows where to find the file.

Manual --log-file (any value) disables the daemon-driven file logging.

Fix: client/ui SetLogLevel looked up proto.LogLevel_value with the lowercase
logrus name, which never matched the uppercase enum keys and silently fell back
to INFO — so trace/debug requests from the bundle flow had no effect.
2026-06-11 18:34:48 +02:00

93 lines
3.2 KiB
Go

//go:build !android && !ios && !freebsd && !js
// Package guilog manages the desktop UI's own file log (gui-client.log), which
// follows the daemon's log level: when the daemon is in debug/trace the GUI
// attaches a rotated file alongside the console so its (and the React frontend's
// forwarded) output is captured for the debug bundle. It is intentionally not a
// Wails service — it has no frontend-facing methods and generates no TS
// bindings — so it lives outside client/ui/services.
package guilog
import (
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
// DebugLog is the daemon-debug-driven GUI file log. The daemon publishes a
// marked "log-level-changed" SystemEvent over SubscribeEvents (both on change
// and once per new subscription, so a daemon already in debug is picked up at
// startup); services.DaemonFeed routes it here via Apply.
//
// When the daemon is in debug/trace and the GUI owns its log (no manual
// --log-file), it attaches a rotated gui-client.log alongside the console and
// raises the logrus level; back to a higher level it detaches the file and
// restores info. The file is left on disk (rotated by timberjack) for the debug
// bundle to collect. When the user set --log-file explicitly, it is disabled and
// never touches logging.
type DebugLog struct {
uiPath string
enabled bool
mu sync.Mutex
fileOn bool
}
// NewDebugLog builds the GUI debug log. uiPath is the absolute gui-client.log
// path; enabled is false when the user passed --log-file (manual override), in
// which case it leaves logging untouched.
func NewDebugLog(uiPath string, enabled bool) *DebugLog {
return &DebugLog{uiPath: uiPath, enabled: enabled}
}
// Path returns the GUI log path to register with the daemon, or "" when the GUI
// doesn't own its log (manual --log-file) — in that case the daemon shouldn't
// try to collect a gui-client.log the GUI never writes.
func (d *DebugLog) Path() string {
if !d.enabled {
return ""
}
return d.uiPath
}
// Apply reacts to a daemon log level (the lowercase logrus name, e.g. "debug").
// Idempotent: repeated identical levels are no-ops, so the startup replay plus a
// racing change-event do no harm.
func (d *DebugLog) Apply(level string) {
if !d.enabled {
return
}
// "debug or more verbose" (debug/trace) turns the file log on; anything less
// verbose turns it off. Compare numerically against logrus' own levels so
// there are no hard-coded level-name literals.
lvl, err := log.ParseLevel(level)
if err != nil {
lvl = log.InfoLevel
}
debug := lvl >= log.DebugLevel
d.mu.Lock()
defer d.mu.Unlock()
switch {
case debug && !d.fileOn:
if err := util.SetLogOutputs(log.StandardLogger(), util.LogConsole, d.uiPath); err != nil {
log.Errorf("attach GUI file log %s: %v", d.uiPath, err)
return
}
log.SetLevel(lvl)
d.fileOn = true
log.Infof("GUI file logging enabled (daemon level %s), writing to %s", level, d.uiPath)
case !debug && d.fileOn:
if err := util.SetLogOutputs(log.StandardLogger(), util.LogConsole); err != nil {
log.Errorf("detach GUI file log: %v", err)
}
log.SetLevel(log.InfoLevel)
d.fileOn = false
log.Infof("GUI file logging disabled (daemon level: %s)", level)
}
}