feat(diagnostics): persistent redacted logging and an exportable bundle
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m12s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 37s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m10s
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m12s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 37s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m10s
The app forgot everything it did the moment it exited. The Rust half
logged through env_logger to stdout only -- invisible to anyone who
launched from a desktop icon, and on Android worse than that: stdout is
not logcat, so the backend produced no visible output at all on the
platform carrying this project's hardest bugs. The autoplay deadlock,
the truncated-stream restart and the background-audio stall were all
diagnosed by talking a user through `adb logcat`, because there was no
other way to see anything. A panic left nothing behind at all.
Logs now go to a size-capped rotating file, to logcat on Android, and to
the webview console in dev. A panic is recorded with its backtrace before
the process dies. The frontend's messages are forwarded into the same
file, so one timeline holds both halves of the app in order -- which is
what makes a race between them legible after the fact, and races between
them are the expensive bug class here.
Redaction runs in the log FORMATTER, not at export time. A credential
sitting in a file on the device is already a disclosure; stripping it on
the way out would be too late. The exporter redacts a second time to
cover files written by builds that predate this. api_key, X-Emby-Token,
Authorization, "AccessToken" and Token="..." all reduce to [REDACTED],
while host, item ids and filenames are deliberately kept -- a log scrubbed
of those is one nobody can debug anything from. Server URLs keep scheme
and host and drop any embedded user:pass@.
Two things the tests caught that review would not have:
- redact_headers recursed on its own output. The replacement keeps the
header NAME, so the next call matched the same header forever; the
test died with a stack overflow. It is a forward scan now.
- The frontend forwarder used `void plugin.error(...)`. `void` discards
a promise's value but not its rejection, so in any webview without
IPC -- a unit test, SSR, a browser preview -- every log line became an
unhandled rejection. 20 of them showed up the first time coverage
ran. Each call now attaches a catch.
Only info and above cross the IPC boundary: debug is per-tick player
state and forwarding it would be thousands of calls a minute for output
nobody reads. A failing forwarder never propagates and never prevents the
console write.
Nothing is transmitted anywhere. The export writes a zip and reports its
path; the user attaches it themselves, which is also what keeps this from
becoming telemetry. An Android share intent is explicitly out of scope --
it is Kotlin work that belongs with the other native code.
The panic hook chains to the previous hook rather than replacing it,
because utils/lock.rs installs a silencing hook around tests that provoke
poisoned locks on purpose.
Spec in docs/specs/diagnostics-and-logging.md; UR-078 / DR-218 / UT-209.
Verified: 1079 frontend tests and the coverage gate, 759 Rust tests,
clippy -D warnings, svelte-check 0 errors, and cargo check for
aarch64-linux-android.
This commit is contained in:
+82
-4
@@ -58,6 +58,10 @@ use commands::{
|
||||
// Device commands
|
||||
device_get_id,
|
||||
device_set_id,
|
||||
// Diagnostics commands
|
||||
diagnostics_export,
|
||||
diagnostics_get_info,
|
||||
diagnostics_set_level,
|
||||
download_album,
|
||||
download_item,
|
||||
download_item_and_start,
|
||||
@@ -993,6 +997,11 @@ fn specta_builder() -> Builder<tauri::Wry> {
|
||||
playlist_add_items,
|
||||
playlist_remove_items,
|
||||
playlist_move_item,
|
||||
// Diagnostics commands
|
||||
// TRACES: UR-078 | DR-218
|
||||
diagnostics_get_info,
|
||||
diagnostics_set_level,
|
||||
diagnostics_export,
|
||||
// Conversion commands
|
||||
format_time_seconds,
|
||||
format_time_seconds_long,
|
||||
@@ -1099,12 +1108,67 @@ fn set_env_if_unset(key: &str, value: &str) {
|
||||
/// through `convertFileSrc` again.
|
||||
///
|
||||
/// TRACES: UR-012, UR-071 | DR-134, DR-137, DR-198
|
||||
/// Build the logging plugin.
|
||||
///
|
||||
/// Replaces the previous `env_logger` init, which wrote to **stdout only**. That
|
||||
/// was invisible to anyone who launched from a desktop icon, and worse than
|
||||
/// useless on Android: stdout is not logcat, so the Rust backend produced no
|
||||
/// visible output at all on the platform carrying the hardest bugs in this
|
||||
/// project's history (the autoplay deadlock, the truncated-stream restart, the
|
||||
/// background-audio stall). tauri-plugin-log routes to logcat there for free.
|
||||
///
|
||||
/// Three decisions worth keeping:
|
||||
///
|
||||
/// * **Every line goes through `redact` first.** A credential must never reach
|
||||
/// disk, not merely be stripped later when a bundle is exported — a file on
|
||||
/// the device is already the disclosure.
|
||||
/// * **The size cap is deliberate.** `RotationStrategy::KeepAll` would let a
|
||||
/// long-running session fill a phone. One rotation keeps yesterday's evidence
|
||||
/// without unbounded growth.
|
||||
/// * **The level is read from disk.** Someone reproducing a bug needs debug
|
||||
/// logging to survive the restart that reproduces it.
|
||||
///
|
||||
/// TRACES: UR-078 | DR-218
|
||||
fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
use tauri_plugin_log::{Target, TargetKind};
|
||||
|
||||
let mut targets = vec![
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some("jellytau".to_string()),
|
||||
}),
|
||||
];
|
||||
|
||||
// Rust lines in the webview console, so a developer sees both halves of the
|
||||
// app in one place. Dev only -- in a release build this would ship backend
|
||||
// logging into a console the user can open.
|
||||
if cfg!(debug_assertions) {
|
||||
targets.push(Target::new(TargetKind::Webview));
|
||||
}
|
||||
|
||||
tauri_plugin_log::Builder::new()
|
||||
.targets(targets)
|
||||
.level(log::LevelFilter::Info)
|
||||
.max_file_size(5 * 1024 * 1024)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}][{}] {}",
|
||||
record.level(),
|
||||
record.target(),
|
||||
crate::utils::diagnostics::redact(&message.to_string())
|
||||
))
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize logger
|
||||
env_logger::Builder::from_default_env()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.init();
|
||||
// Crash capture before anything else, so a panic during startup is recorded
|
||||
// rather than vanishing with the process.
|
||||
//
|
||||
// TRACES: UR-078 | DR-218
|
||||
crate::utils::diagnostics::install_panic_hook();
|
||||
|
||||
// On Linux, video plays through WebKitGTK's HTML5 <video> element, which uses
|
||||
// GStreamer as its media backend. Enable hardware-accelerated (VAAPI) decoding
|
||||
@@ -1121,6 +1185,7 @@ pub fn run() {
|
||||
let invoke_handler = builder.invoke_handler();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(build_log_plugin())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.invoke_handler(invoke_handler)
|
||||
@@ -1139,6 +1204,19 @@ pub fn run() {
|
||||
// replace an installed APK, and the frontend offers the releases
|
||||
// page there instead.
|
||||
//
|
||||
// Re-apply the log level the user last chose. Without this the
|
||||
// picker would only affect the running session -- and the whole
|
||||
// point of a persisted level is that somebody reproducing a bug
|
||||
// keeps debug logging across the restart that reproduces it.
|
||||
//
|
||||
// TRACES: UR-078 | DR-218
|
||||
if let Ok(config_dir) = app.path().app_config_dir() {
|
||||
if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
|
||||
log::set_max_level(level);
|
||||
log::info!("[DIAG] restored log level from settings: {level}");
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: UR-077 | DR-217
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user