9d14e63613
Add an Application Logs window backed by a bounded persistent main log file. The viewer loads history from lanspread.log, subscribes to live INFO/WARN/ERROR log events, supports filtering/copy/pause controls, and keeps the menu/window routing separate from the unpack log viewer. The backend sink now owns serialized access to the log file. History reads and append-time trimming use the same sink lock, so opening the logs window cannot race with a concurrent write and rewrite away a freshly appended line. The sink also keeps a persistent file handle instead of reopening the file for each captured event. Live log events carry sink-local sequence ids. The frontend uses the history watermark plus returned history line counts to suppress live events that were already included in the history response, while preserving buffered rows that were trimmed out of the history file. Auto-scroll now follows the last visible row identity, so it continues following after the in-memory cap keeps the row count stable. No timestamp code change was needed. On the Linux dev host, a temporary probe showed time::OffsetDateTime::now_local() returning +02:00 while UTC was +00:00, matching the host CEST offset. Test Plan: - just fmt - just frontend-test - just test - just clippy - just build - git diff --cached --check - temporary Linux probe of OffsetDateTime::now_local() showed local +02:00 Refs: none
13 KiB
13 KiB
sessionId
| sessionId |
|---|
| session-260605-123454-ub3w |
Requirements
Overview & Goals
Add a "main log output" viewer so users can inspect the application's runtime logs. Logs must still be written to stdout, must also be captured to a persistent single file in the app data directory, must be bounded to the last ~2 MB, and must be accessible via a dedicated window opened from the main UI.
Scope
In Scope:
- Capture
log::andtracing::output to a persistent log file in the app data directory. - Enforce ~2 MB size limit on the stored log (keep the most recent data).
- Expose a Tauri command to retrieve the current log content.
- Add a new companion window (
?view=main-logs) with a viewer UI modeled on the existing unpack logs feature. - Support live tailing of the logs, displaying new log entries as they are emitted from the backend in real time.
- Support dynamic log level filtering in the UI to toggle/filter visible logs by log level (TRACE, DEBUG, INFO, WARN, ERROR).
- Add "Application logs" entry to the main window kebab menu that opens/focuses the logs window.
Out of Scope:
- Configurable log levels or per-module filters in the backend.
- Multi-file rotation or archival of old logs.
- Persisting logs across app reinstalls or explicit export.
User Stories
- As a user troubleshooting an issue, I want to open the application logs from the menu so that I can see recent log output without needing external tools.
- As a power user, I want the log file to stay bounded (~2 MB) so that it does not grow unbounded on disk.
- As a developer or user experiencing an application crash, I want the logs to be persistently saved to a file on disk so that I can inspect them externally even if the application UI cannot be opened or crashes.
Functional Requirements
- Logger writes to stdout, app-data file target (
lanspread.log), and a Tauri event stream for real-time UI tailing. - Log file is kept at ~2 MB; older content is dropped to keep the tail.
get_main_logscommand returns up to the last 2 MB of log text.- Main logs window renders logs in real-time, automatically appending new log lines.
- Auto-scroll to the bottom of the log viewer container when new lines are appended (active by default, can be toggled).
- Pause/Resume control to freeze the log stream so the user can easily scroll, select, and read without being interrupted by new lines.
- Case-insensitive regex filtering of the displayed lines, using the exact same regex input validation, compilation, and error styling (red input border and error label) as the unpack logs window.
- Support dynamic log level filtering in the UI via a
SegmentedRadiofilter with options: "All", "Debug+", "Info+", "Warn+", "Error only". - Support copying all/filtered logs to the clipboard and clearing the active log window's in-memory rows. Clearing the viewer does not delete the persisted log file.
- Window title: "Application Logs"; label: "main-logs".
- Menu item appears in the same kebab menu as "Unpack logs".
Technical Design
Current Implementation
- Logging setup:
crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2223createstauri_plugin_log::Builderwith onlyTargetKind::Stdout+LevelFilter::Info(plus mdns off). Plugin registered at 2237. - State & persistence pattern:
LanSpreadStateholdsunpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>,state_dir: OnceLock<PathBuf>;load_unpack_logs/persist_unpack_logs+trim_unpack_logs(MAX=20) in the same file; exposed byget_unpack_logs(153). - UI pattern:
src/windows/MainWindow.tsx:23definesopenLogsWindowusingWebviewWindowwith labelunpack-logsand?view=unpack-logs; menu item at 110;src/App.tsx:9dispatches viaisUnpackLogsView; full viewer insrc/UnpackLogsWindow.tsx(filtering, list+detail, regex). - The repo uses mostly
log::in the Tauri and peer crates, with sometracing::in shared crates. The current Tauri app does not bridge or subscribe totracing::output.
Key Decisions
- Store the main log as a single plain-text file at
app.path().app_data_dir()?.join("lanspread.log"). - Do not use
TargetKind::LogDirfor this feature. It writes to the OS log directory and uses file replacement rotation, which does not match the requested app-data location or "keep the latest tail" behavior. - Replace the current
tauri_plugin_loglogger setup with an app-owned logging setup initialized during.setupafter the app data directory has been resolved.- Install a
tracing_subscribersubscriber as the single fan-out path for stdout, persistent file writes, and live UI events. - Install
tracing_log::LogTracerso existinglog::calls are captured by the same subscriber astracing::events. - If
tauri-plugin-logis kept registered for future frontend log commands, configure it withskip_logger()so it does not compete for the global logger.
- Install a
- Use one backend formatter for both file and live-event output. Format lines as:
[YYYY-MM-DD][HH:MM:SS][target][LEVEL] message- This makes historical and live rows comparable and makes client-side level parsing deterministic.
- Implement a shared
MainLogSinkthat:- Appends formatted lines to
lanspread.log. - Emits the same formatted line to the frontend via a Tauri event, e.g.
main-log-line. - Bounds the file to the latest ~2 MB by rewriting the tail when the file exceeds the configured limit.
- Never logs from inside the logging path; write/emit failures are ignored or recorded without recursive logging.
- Appends formatted lines to
- Handle the transition from loading history to receiving real-time logs in
MainLogsWindow.tsxby using a buffering and deduplication strategy:- Register the
main-log-linelistener before requesting history. - Buffer incoming live rows while the initial
get_main_logsfile history is being requested. - Once history is loaded, append buffered rows after removing any rows already present in the loaded history by exact line match.
- Transition to direct real-time appending with auto-scrolling to the bottom (if auto-scroll is enabled).
- Register the
- Implement dynamic log level filtering client-side using the existing
SegmentedRadiocomponent.- For live-streamed logs, use the structured event payload's level field.
- For historical log files loaded via
get_main_logs, parse the level from the controlled[LEVEL]field, falling back toInfo.
- Provide UI controls for "Pause / Resume", "Auto-scroll", and "Log Level Filter" to ensure high-quality user experience during busy logging periods.
- Model the new viewer on the unpack-logs style, retaining the identical case-insensitive regex filter ability and regex error reporting UI, but customize it for linear line streaming (sans split-pane list+detail structure, since main logs are a single continuous log flow).
Proposed Changes
- Rust dependencies: add
tracing-subscriberandtracing-logto the workspace/Tauri crate. - Rust (lib.rs): initialize the custom logging pipeline in
.setup, add boundedMainLogSink, addget_main_logs, emitmain-log-line, and register the command. - Tauri capabilities: add a
main-logscapability file for the new window label withcore:default. - Frontend (MainLogsWindow.tsx): implement the scrollable log viewport using
listen('main-log-line')for live tailing, supporting buffering/deduplication, pause/resume, auto-scroll, regex filtering, copying, and clearing. - Frontend (App.tsx & MainWindow.tsx): dispatch the new companion view and add the kebab menu entry to open the window.
- No changes to peer crate, db, or protocol.
File Structure
- Modified:
Cargo.toml,Cargo.lock,crates/lanspread-tauri-deno-ts/src-tauri/Cargo.toml,crates/lanspread-tauri-deno-ts/src-tauri/Cargo.lock,crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs,crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx,crates/lanspread-tauri-deno-ts/src/App.tsx - Added:
crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx(and.cssif custom styles needed beyond shared) - Added:
crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json
Risks
- Trimming on every write would be expensive; mitigate by trimming only when the file exceeds the 2 MB limit plus a small slack threshold, then rewriting the latest tail.
- The logging path is synchronous and global; keep it small, lock-protected, and non-recursive.
- Log lines may contain very long messages; UI should handle wrapping/horizontal scrolling with stable dimensions.
- On Windows release builds the console is hidden, so file becomes the only log source — acceptable.
- Installing
LogTracerplus atracing_subscribersubscriber must happen once. Tests should avoid double-initializing global logging.
Delivery Steps
Step 1: extend-logger-and-add-get-main-logs-command
Logger writes to stdout, persistent app-data lanspread.log, and a live UI event stream. A bounded sink enforces the 2 MB tail.
- Add workspace/Tauri dependencies for
tracing-subscriberandtracing-log. - Add constants
MAIN_LOG_FILE_NAME: &str = "lanspread.log"andMAX_MAIN_LOG_BYTES: u64 = 2 * 1024 * 1024near the unpack log consts. - Implement
main_log_path(state_dir: &Path) -> PathBuf. - Implement
trim_main_log_file(path: &Path)that checks file size and rewrites only the last ~2 MB if exceeded, preserving valid UTF-8 by trimming to a character boundary when needed. - Implement a
MainLogSinkor equivalent shared target that formats each log record once, appends it tolanspread.log, emitsmain-log-line, and trims when the file grows past the limit plus slack. - Initialize logging during
.setupafter resolvinglet state_dir = app.path().app_data_dir()?and creating the app data directory.- Install
LogTracerforlog::macros. - Install a
tracing_subscribersubscriber/layer that writes to stdout and the shared main-log sink. - Preserve the current global level behavior:
Infoby default andmdns_sd::service_daemonoff.
- Install
- Implement
#[tauri::command] async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String>that resolvesapp_handle.path().app_data_dir(), trimslanspread.logif needed, reads the file (or returns empty string), and returns the content. - Add
get_main_logsto theinvoke_handler!macro list. - Add focused unit tests for tail trimming and level parsing/formatting helpers where practical.
Step 2: implement-main-logs-frontend-window-and-dispatch
Implement MainLogsWindow with loading + real-time streaming and handle routing.
- Create
crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx:- Fetch existing logs via
invoke('get_main_logs')on load and split into an array of lines. - Register
listen<MainLogLinePayload>('main-log-line', ...)from@tauri-apps/api/eventbefore fetching history. - Buffer incoming logs while loading history, and append/deduplicate them once loaded.
- Store logs as objects containing the log line text and its associated
LogLevel. - Extract
LogLevelfor live logs from the event payload. For historical logs, parseLogLevelfrom the controlled[LEVEL]field (default to Info). - Cap in-memory rows/bytes so a long-running logs window does not grow without bound.
- Render the log lines in a scrollable view.
- Implement the identical case-insensitive regex filter ability on lines (compiling regex with error catching, displaying error strings below header, and toggling an invalid input class name).
- Implement dynamic log level filtering in the UI using the shared
SegmentedRadiocomponent with options: "All", "Debug+", "Info+", "Warn+", "Error only". - Implement UI controls: "Auto-scroll" checkbox (scrolls viewport to bottom when new logs arrive, active by default), "Pause / Resume" toggle (buffers/pauses incoming stream rendering), "Clear" button, "Copy to clipboard" button.
- Fetch existing logs via
- Implement
isMainLogsView()helper that checks theview=main-logsquery param (export it). - Update
crates/lanspread-tauri-deno-ts/src/App.tsxto importMainLogsWindow, isMainLogsViewand dispatch to it when the query matches (before falling back to MainWindow). - Create
MainLogsWindow.cssfor styling the viewer, search bars, controls, and scrollable log pane.
Step 3: add-main-logs-capability
The new companion window can invoke commands and listen for log events.
- Add
crates/lanspread-tauri-deno-ts/src-tauri/capabilities/main-logs.json. - Set
"windows": ["main-logs"]. - Grant
"permissions": ["core:default"]. - No
log:defaultpermission is needed because the UI listens to the app-ownedmain-log-lineevent via Tauri core events.
Step 4: add-menu-item-and-window-opener-in-mainwindow
Users can open the main logs viewer from the existing kebab menu.
- In
crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx, add anopenMainLogsWindowasync function (copy ofopenLogsWindowbut with label'main-logs', title'Application Logs', url'/?view=main-logs'). - Add a new
KebabItemfor'Application logs'that callsopenMainLogsWindow, placed near the existing'Unpack logs'entry in thekebabItemsarray. - Verify that opening the window focuses an existing instance if already open (same pattern as unpack logs).
Step 5: verify
Use the repo's just commands.
just fmtjust clippyjust frontend-testjust test- Manual smoke test with
just run: open "Application logs", verify history loads from app-datalanspread.log, new backend logs append live, filters work, pause/resume works, copy works, and the file remains bounded near 2 MB.