Skip to content

Add a live Logs window and a Verbose logging switch - #154

Merged
itz4blitz merged 2 commits into
mainfrom
feat/log-panel
Nov 3, 2026
Merged

itz4blitz merged 2 commits into
mainfrom
feat/log-panel

Conversation

@itz4blitz

Copy link
Copy Markdown
Collaborator

Until now the only way to see logs was Settings › About › Save debug log and then opening files. This adds a window that shows them as they're written.

Logs window. Open it from Help › Logs, Ctrl+Shift+L (⇧⌘L on macOS), or View logs in Settings › About.

  • Live tail of tinyclient.log, chatd.log and mediad.log. There are tabs for each process and an All tab that interleaves them.
  • Filters by text and by level (Error / Warn / Info / Debug). Hide noise is on by default and hides GPUI's "RefCell already borrowed" and "No monitor detected!" lines.
  • Pause stops the list from scrolling, and Resume shows how many lines came in meanwhile. Copy visible copies what's shown. Save debug log and Show in Explorer work as before.
  • Memory is bounded: the window keeps the last 5000 lines per process. The first read takes at most 1 MiB from the end of each file. It handles log rotation and lines that are only partly written.
  • It reads the files every 500 ms off the main thread, and only while the window is open. A file watcher would have meant a new dependency. It only shows what's already in the files, and those lines have already been through log_policy::render/redact.

Verbose logging is a switch in the Logs window and a new row in Settings › About (B4). It reuses the existing stored log_level setting, so the protocol needs no new Request:

  • chatd already moves its own level when a setting is stored.
  • The window moves when it reloads settings, which it already does on Change::Settings.
  • New: chatd sends MediaRequest::LogLevel { level } to a running call's or mic test's mediad. This is a new variant on chatd's private stdin protocol with mediad, which ships in the same build.
  • New: share_session puts TINYCLIENT_LOG_LEVEL on helpers, so a mediad started later begins at the right level.
  • New: LogGuard::apply writes a "log level is now debug" line to the file whenever the level changes, so the switch visibly takes effect in each process.

All of this happens without a restart, and the setting is kept for the next start. Turning it off clears the row. If TINYCLIENT_LOG_LEVEL alone would keep the level at debug, it stores info instead.

Docs updated: docs/logging.md, and B2/B4 in docs/product/09-settings.md.

Tests

  • New unit tests for the pure parts in log_tail.rs: level parsing, line splitting, the ring bound, filters, and following a file (including rotation and the first read from the middle of a file). Also tests for level_word/verbose and the LogLevel frame.
  • cargo test -p protocol, -p tinyclient (with and without gui), -p mediad --lib and -p chatd all pass. The one exception is invite_ipc::second_launch_delivers_to_the_primary, which fails here only because an installed client holds the pipe.

Checked by hand on Windows with a debug build and a scratch TINYCLIENT_DIR:

  • The window opens from View logs.
  • Verbose logging persisted across a restart.
  • Toggling it off and on wrote "log level is now info/debug" lines from both tinyclient and chatd, and those lines appeared live in the window.
  • The Warn filter, Pause/Resume with a count of new lines, and Copy visible all work.

🤖 Generated with Claude Code

… Verbose logging switch that takes effect in all three without a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unRekable
unRekable previously approved these changes Nov 3, 2026

@unRekable unRekable left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved ✅ (0 critical, 0 blocking, 4 suggestions) — read the full diff plus surrounding code.

💡 Suggestions

  • log_tail.rs:155 — Rotation can slip through when the fresh live file grows past the old offset within one 500 ms poll (needs roughly sustained multi-MiB/s output), and the read then silently starts mid-file. A version/id check (creation time or first-line fingerprint) would make roll detection exact.
  • gui/logs.rs:217 — Filter edits while paused recompute visible from self.tail, which already holds the lines that arrived during the pause, so pauses are not fully still. Clamping visible to the paused seq would make the semantics uniform.
  • log_policy.rs:313 — TINYCLIENT_LOG_LEVEL on share_session children is currently untested; one get_envs assertion keyed on a guard being installed would cover it.
  • log_tail.rs:241 — contains_folded folds ASCII only, so the filter doc slightly overstates the case-insensitivity for non-ASCII terms. Either say ASCII, or fold via char::to_lowercase (keep the ASCII fast path).

✅ Looks Good

  • The tail engine handles the hard cases for real: backfill of at most 1 MiB from the end dropping the mid-line it lands in, rotation (writer reopens the fresh live file, len shrinks), 64 KiB-capped partials, UTF-8 multibyte splits across reads, 5000-line bound per source — each with a unit test.
  • The redaction guarantee holds end to end: the window only reads files whose lines already went through log_policy::render (redact + cut + denylist), and no user-controlled path is involved.
  • Verbose logging reuses the existing relevel pattern consistently; unknown words leave mediad's ceiling alone, both probe and serve answer Done, and the LogLevel wire frame round-trips (old frames still parse).
  • Polling claims check out: reads run on the background executor, the loop breaks when the window entity is gone, per-poll bytes are bounded, memory is ring-bounded and tested.

CI note: both failing test jobs are the chatd --test playback timing flake (paused at 602 vs a ≤600 bound right after a 400 ms sleep). I re-ran a_pause_lets_the_process_go_and_play_goes_on_from_there from a checkout of this branch — it passes on Windows. The diff touches no playback code. Please re-run the failed jobs so the branch goes green.

let len = file.metadata()?.len();
let (from, mid_line) = match self.offset {
None => (len.saturating_sub(BACKFILL), len > BACKFILL),
Some(offset) if offset > len => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: Roll detection via offset > len alone can miss a rotation whose fresh live file is already past the old offset by the next 500 ms poll (roughly a multi-MiB/s trace storm): the read then silently starts mid-file and drops the head of the fresh log. A version/id check — compare creation time, or fingerprint the first line against the previous file's first line — would make this exact. Rare and diagnostic-only, so non-blocking.

}

/// Draws the filter over the tail again, and follows the end unless paused.
fn refresh(&mut self, cx: &mut Context<Self>) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: While paused, refresh() recomputes visible from self.tail, which already contains the lines that arrived during the pause (they bypassed the snapshot only in take()). Editing the search text, level or source while paused therefore reveals pending lines while paused_at still says they are unconsumed. Clamping visible to seq <= paused_at while paused would make the hold-still promise hold for filter edits too.

/// set the variable, so inheritance alone would give mediad and login a different one. The
/// ceiling goes too: a stored Verbose logging lives in chatd's settings, which a helper never
/// reads, so a mediad started after it was turned on starts at debug.
pub fn share_session(command: &mut std::process::Command) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: The new TINYCLIENT_LOG_LEVEL env var set by share_session currently has no test — share_session_puts_the_id_on_the_child only asserts TINYCLIENT_SESSION, and since unit tests share process state nothing else would catch a regression here. One assertion on get_envs() when a guard is installed would do.

pub source: Option<Source>,
/// The most detailed level shown; Debug includes trace.
pub level: Level,
/// Shown only if the line holds these words, in any case. Empty keeps everything.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: contains_folded folds ASCII bytes only, so the doc here slightly overstates the case-insensitivity for non-ASCII search terms. Either narrow the doc to ASCII, or fold via char::to_lowercase with the ASCII fast path kept for the common case.

… a little later.

Windows CI downloaded the seeded avatar while Discord was still offline, then the READY avatar after connect, so the own-picture test saw two fetches. macOS CI paused a one-second file at 602 ms, two milliseconds outside the old 600 ms window.
@itz4blitz
itz4blitz merged commit 6fc1aee into main Nov 3, 2026
7 checks passed
@itz4blitz
itz4blitz deleted the feat/log-panel branch November 3, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants