App Debug Log¶
The AppDebugLog is a shared ring buffer that receives log lines from all three C++/Rust/TypeScript subsystems and emits them as Tauri events for the in-app console.
Sources: src-tauri/src/torrent_stream/debug_log.rs, src-tauri/src/torrent_stream/debug_api.rs
AppDebugLog struct¶
pub struct AppDebugLog {
app: AppHandle,
enabled: AtomicBool,
lines: Mutex<VecDeque<DebugLine>>,
max_lines: usize, // 2500 (DEFAULT_MAX_LINES)
}
DebugLine¶
#[derive(Clone, Serialize)]
pub struct DebugLine {
pub ts_ms: u64, // Unix timestamp in milliseconds
pub category: String, // "vozduxan" | "soulseek" | "stream" | …
pub message: String,
pub detail: Option<serde_json::Value>, // optional structured data
}
detail is serialized as JSON and omitted from the Tauri event if None (via skip_serializing_if).
Shared Arc — critical wiring¶
AppDebugLog is created once in lib.rs and shared via Arc::clone across three Tauri states:
let debug_log = Arc::new(AppDebugLog::new(app.handle().clone()));
app.manage(VozduxanStreamState::new(&app, debug_log.clone()));
app.manage(TorrentImageState::new(&app, debug_log.clone()));
app.manage(TorrentStreamState::new(&app, debug_log.clone()));
All three must share the same Arc. A separate Arc for any of them would route their logs to a different ring buffer and make those logs invisible in the debug console (see Architecture Overview — critical invariant).
The C++ logger is also wired to this same sink: VozduxanConfig.log_fn is a function pointer that calls on_vozduxan_log(), which calls AppDebugLog.push("vozduxan", …). The C++ log_userdata pointer points into the Arc<AppDebugLog> kept alive by VozduxanSessionInner._debug_log_arc.
push is always-on¶
pub fn push(&self, category: impl Into<String>, message: impl Into<String>, detail: Option<serde_json::Value>) {
// ... build DebugLine with current timestamp ...
{
let mut q = self.lines.lock().unwrap();
while q.len() >= self.max_lines { q.pop_front(); }
q.push_back(line.clone());
}
let _ = self.app.emit("app-debug-line", &line);
}
is_enabled no longer gates writes. Every log call always writes to the ring buffer and emits app-debug-line. This ensures errors are captured even when the debug window was never opened. set_enabled now only controls whether stderr (eprintln!) receives duplicate output in some paths — it is kept for the legacy setting file migration.
Log sources¶
| Source | How logs reach the sink |
|---|---|
| C++ vozduxan | VozduxanConfig.log_fn → on_vozduxan_log() → push("vozduxan", …) |
| Rust vozduxan_stream | self.dlog(msg) / dlog closure in spawn_blocking |
| Rust torrent_image | self.dlog(msg) |
| Rust soulseek | session.slog(msg) → push("soulseek", …) |
| Frontend TypeScript | appDebugLog(category, msg) → app_debug_push Tauri command |
appDebugLog in the frontend batches calls into the app_debug_push command which calls push on the shared log. This is how src/torrent/api.ts logs stream events (see Prefetch).
stderr always receives all logs from Rust and C++ (eprintln! / C++ stderr). The ring buffer is in addition to stderr, not a replacement.
Tauri commands¶
| Command | Purpose |
|---|---|
get_app_debug_enabled |
Returns is_enabled() |
set_app_debug_enabled(enabled) |
Sets enabled flag + persists to app_debug.json |
get_app_debug_log |
Returns snapshot() — full buffer oldest-first |
clear_app_debug_log |
Clears in-memory buffer |
app_debug_push(category, message, detail) |
Frontend → ring buffer bridge |
Persistence¶
enabled is persisted to {exe_dir}/app_debug.json:
On startup, apply_app_debug_from_disk reads this file and calls set_enabled. A legacy path (streaming_debug.json in app_data_dir) is read as a fallback for one-time migration.
In-app console¶
AppDebugConsole.vue renders the ring buffer. It listens for app-debug-line Tauri events to append lines in real-time without polling. The console is shown in AppDebugWindow.vue which is mounted when the ?app-debug window URL is opened.
In dev builds, App.vue opens the debug window automatically. In production there is no UI to open it — it requires navigating to the ?app-debug URL directly or using the debug panel in Settings if exposed.