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.
This commit is contained in:
Zoltán Papp
2026-06-11 18:33:21 +02:00
parent 96f0c7a165
commit ff6aef5e2a
17 changed files with 971 additions and 419 deletions

View File

@@ -40,6 +40,25 @@ func TestAddRotatedLogFiles_PicksUpAllVariants(t *testing.T) {
require.NotContains(t, names, "other.log", "unrelated files should not be in bundle")
}
// TestAddRotatedLogFiles_GUIPrefix asserts the prefix parameter scopes the glob
// to the GUI log: gui-client.log.* rotated siblings are picked up and the
// daemon's own client.log.* are not (and vice versa, covered above). This is
// the load-bearing check for the gui-client.log bundle collection — the old
// "client*.log.*" glob would have missed gui-client rotations.
func TestAddRotatedLogFiles_GUIPrefix(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "gui-client.log.1"), "gui rotated\n")
writeGzFile(t, filepath.Join(dir, "gui-client.log.2.gz"), "gui rotated gz\n")
writeFile(t, filepath.Join(dir, "client.log.1"), "daemon rotated\n")
names := runAddRotatedLogFilesPrefix(t, dir, "gui-client", 10)
require.Contains(t, names, "gui-client.log.1", "gui-client rotated file should be in bundle")
require.Contains(t, names, "gui-client.log.2.gz", "gui-client gz rotated file should be in bundle")
require.NotContains(t, names, "client.log.1", "daemon rotated file must not match the gui-client prefix")
}
// TestAddRotatedLogFiles_RespectsLogFileCount asserts that only the newest
// logFileCount rotated files are bundled, ordered by mtime.
func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) {
@@ -67,6 +86,10 @@ func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) {
// runAddRotatedLogFiles calls addRotatedLogFiles against a fresh in-memory
// zip writer and returns the set of entry names that ended up in the archive.
func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[string]struct{} {
return runAddRotatedLogFilesPrefix(t, dir, "client", logFileCount)
}
func runAddRotatedLogFilesPrefix(t *testing.T, dir, prefix string, logFileCount uint32) map[string]struct{} {
t.Helper()
var buf bytes.Buffer
@@ -74,7 +97,7 @@ func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[st
archive: zip.NewWriter(&buf),
logFileCount: logFileCount,
}
g.addRotatedLogFiles(dir)
g.addRotatedLogFiles(dir, prefix)
require.NoError(t, g.archive.Close())
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))