Add application log viewer
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
---
|
||||
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::` and `tracing::` 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_logs` command 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 `SegmentedRadio` filter 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:2223` creates `tauri_plugin_log::Builder` with only `TargetKind::Stdout` + `LevelFilter::Info` (plus mdns off). Plugin registered at 2237.
|
||||
- State & persistence pattern: `LanSpreadState` holds `unpack_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 by `get_unpack_logs` (153).
|
||||
- UI pattern: `src/windows/MainWindow.tsx:23` defines `openLogsWindow` using `WebviewWindow` with label `unpack-logs` and `?view=unpack-logs`; menu item at 110; `src/App.tsx:9` dispatches via `isUnpackLogsView`; full viewer in `src/UnpackLogsWindow.tsx` (filtering, list+detail, regex).
|
||||
- The repo uses mostly `log::` in the Tauri and peer crates, with some `tracing::` in shared crates. The current Tauri app does not bridge or subscribe to `tracing::` 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::LogDir` for 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_log` logger setup with an app-owned logging setup initialized during `.setup` after the app data directory has been resolved.
|
||||
- Install a `tracing_subscriber` subscriber as the single fan-out path for stdout, persistent file writes, and live UI events.
|
||||
- Install `tracing_log::LogTracer` so existing `log::` calls are captured by the same subscriber as `tracing::` events.
|
||||
- If `tauri-plugin-log` is kept registered for future frontend log commands, configure it with `skip_logger()` so it does not compete for the global logger.
|
||||
- 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 `MainLogSink` that:
|
||||
- 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.
|
||||
- Handle the transition from loading history to receiving real-time logs in `MainLogsWindow.tsx` by using a **buffering and deduplication strategy**:
|
||||
- Register the `main-log-line` listener before requesting history.
|
||||
- Buffer incoming live rows while the initial `get_main_logs` file 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).
|
||||
- Implement **dynamic log level filtering client-side** using the existing `SegmentedRadio` component.
|
||||
- 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 to `Info`.
|
||||
- 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-subscriber` and `tracing-log` to the workspace/Tauri crate.
|
||||
- **Rust (lib.rs):** initialize the custom logging pipeline in `.setup`, add bounded `MainLogSink`, add `get_main_logs`, emit `main-log-line`, and register the command.
|
||||
- **Tauri capabilities:** add a `main-logs` capability file for the new window label with `core: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 `.css` if 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 `LogTracer` plus a `tracing_subscriber` subscriber 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-subscriber` and `tracing-log`.
|
||||
- Add constants `MAIN_LOG_FILE_NAME: &str = "lanspread.log"` and `MAX_MAIN_LOG_BYTES: u64 = 2 * 1024 * 1024` near 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 `MainLogSink` or equivalent shared target that formats each log record once, appends it to `lanspread.log`, emits `main-log-line`, and trims when the file grows past the limit plus slack.
|
||||
- Initialize logging during `.setup` after resolving `let state_dir = app.path().app_data_dir()?` and creating the app data directory.
|
||||
- Install `LogTracer` for `log::` macros.
|
||||
- Install a `tracing_subscriber` subscriber/layer that writes to stdout and the shared main-log sink.
|
||||
- Preserve the current global level behavior: `Info` by default and `mdns_sd::service_daemon` off.
|
||||
- Implement `#[tauri::command] async fn get_main_logs(app_handle: tauri::AppHandle) -> tauri::Result<String>` that resolves `app_handle.path().app_data_dir()`, trims `lanspread.log` if needed, reads the file (or returns empty string), and returns the content.
|
||||
- Add `get_main_logs` to the `invoke_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/event` before 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 `LogLevel` for live logs from the event payload. For historical logs, parse `LogLevel` from 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 `SegmentedRadio` component 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.
|
||||
- Implement `isMainLogsView()` helper that checks the `view=main-logs` query param (export it).
|
||||
- Update `crates/lanspread-tauri-deno-ts/src/App.tsx` to import `MainLogsWindow, isMainLogsView` and dispatch to it when the query matches (before falling back to MainWindow).
|
||||
- Create `MainLogsWindow.css` for 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:default` permission is needed because the UI listens to the app-owned `main-log-line` event 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 an `openMainLogsWindow` async function (copy of `openLogsWindow` but with label `'main-logs'`, title `'Application Logs'`, url `'/?view=main-logs'`).
|
||||
- Add a new `KebabItem` for `'Application logs'` that calls `openMainLogsWindow`, placed near the existing `'Unpack logs'` entry in the `kebabItems` array.
|
||||
- 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 fmt`
|
||||
- `just clippy`
|
||||
- `just frontend-test`
|
||||
- `just test`
|
||||
- Manual smoke test with `just run`: open "Application logs", verify history loads from app-data `lanspread.log`, new backend logs append live, filters work, pause/resume works, copy works, and the file remains bounded near 2 MB.
|
||||
Reference in New Issue
Block a user