Compare commits
3 Commits
72659eab2e
...
8a8437036d
| Author | SHA1 | Date | |
|---|---|---|---|
|
8a8437036d
|
|||
|
639a06e224
|
|||
|
27a054fcb7
|
-155
@@ -1,155 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
Generated
+6
-6
@@ -6,8 +6,8 @@
|
|||||||
"npm:@tauri-apps/plugin-dialog@^2.7.1": "2.7.1",
|
"npm:@tauri-apps/plugin-dialog@^2.7.1": "2.7.1",
|
||||||
"npm:@tauri-apps/plugin-shell@^2.3.5": "2.3.5",
|
"npm:@tauri-apps/plugin-shell@^2.3.5": "2.3.5",
|
||||||
"npm:@tauri-apps/plugin-store@^2.4.3": "2.4.3",
|
"npm:@tauri-apps/plugin-store@^2.4.3": "2.4.3",
|
||||||
"npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.16",
|
"npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.17",
|
||||||
"npm:@types/react@^19.2.16": "19.2.16",
|
"npm:@types/react@^19.2.17": "19.2.17",
|
||||||
"npm:@vitejs/plugin-react@^6.0.2": "6.0.2_vite@8.0.16",
|
"npm:@vitejs/plugin-react@^6.0.2": "6.0.2_vite@8.0.16",
|
||||||
"npm:react-dom@^19.2.7": "19.2.7_react@19.2.7",
|
"npm:react-dom@^19.2.7": "19.2.7_react@19.2.7",
|
||||||
"npm:react@^19.2.7": "19.2.7",
|
"npm:react@^19.2.7": "19.2.7",
|
||||||
@@ -226,14 +226,14 @@
|
|||||||
"tslib"
|
"tslib"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/react-dom@19.2.3_@types+react@19.2.16": {
|
"@types/react-dom@19.2.3_@types+react@19.2.17": {
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/react"
|
"@types/react"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/react@19.2.16": {
|
"@types/react@19.2.17": {
|
||||||
"integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==",
|
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"csstype"
|
"csstype"
|
||||||
]
|
]
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
"npm:@tauri-apps/plugin-shell@^2.3.5",
|
"npm:@tauri-apps/plugin-shell@^2.3.5",
|
||||||
"npm:@tauri-apps/plugin-store@^2.4.3",
|
"npm:@tauri-apps/plugin-store@^2.4.3",
|
||||||
"npm:@types/react-dom@^19.2.3",
|
"npm:@types/react-dom@^19.2.3",
|
||||||
"npm:@types/react@^19.2.16",
|
"npm:@types/react@^19.2.17",
|
||||||
"npm:@vitejs/plugin-react@^6.0.2",
|
"npm:@vitejs/plugin-react@^6.0.2",
|
||||||
"npm:react-dom@^19.2.7",
|
"npm:react-dom@^19.2.7",
|
||||||
"npm:react@^19.2.7",
|
"npm:react@^19.2.7",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@tauri-apps/plugin-shell": "^2.3.5"
|
"@tauri-apps/plugin-shell": "^2.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.16",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export RUSTFLAGS := "-C target-cpu=native"
|
|||||||
export WEBKIT_DISABLE_COMPOSITING_MODE := "1"
|
export WEBKIT_DISABLE_COMPOSITING_MODE := "1"
|
||||||
export DOCKER_CONFIG := env_var_or_default("DOCKER_CONFIG", ".lanspread-peer-cli/docker-config")
|
export DOCKER_CONFIG := env_var_or_default("DOCKER_CONFIG", ".lanspread-peer-cli/docker-config")
|
||||||
|
|
||||||
|
default: run
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
cargo install tauri-cli
|
cargo install tauri-cli
|
||||||
cd crates/lanspread-tauri-deno-ts && deno install --frozen=true
|
cd crates/lanspread-tauri-deno-ts && deno install --frozen=true
|
||||||
|
|||||||
Reference in New Issue
Block a user