Skip to content

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_fnon_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:

{ "enabled": true }

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.