feat(bridge): tee tracing output to platform log file + log_file_path_str API
Bridge already had a tracing fmt layer writing to stderr but a Flutter
desktop app launched from Explorer / RDP has no terminal attached so
those records vanish. Add a non-ANSI file appender (best-effort,
4 MiB rotation) at the platform-conventional log path so developers
and beta testers can hand-inspect output:
* Linux: $XDG_STATE_HOME/app.chanora/chanora_flutter/chanora.log
(fallback ~/.local/state/...)
* macOS: ~/Library/Logs/app.chanora.chanora_flutter/chanora.log
* Windows: %LOCALAPPDATA%\app.chanora\chanora_flutter\logs\chanora.log
Expose log_file_path_str() over FRB so the UI can show the path in a
'Save diagnostics' affordance later. Mobile (Android/iOS) returns an
empty string — those platforms still rely on logcat / Console.app.
No DEC-016 conflict: this is local-only, append-only, never
auto-uploaded. The in-memory log sink and export_diagnostics() path
are unchanged. The redacting layer still wraps the in-memory sink;
the new file appender consumes the same tracing events post-filter.
Trigger for this change: a ko-KR Windows 11 tester saw
'BridgeError.invalidCommand(audio not started)' with no way to find
the upstream warn record that documents which cpal call failed. Log
file is now discoverable without a terminal launch.
This commit is contained in:
@@ -92,14 +92,120 @@ pub fn bridge_init() {
|
||||
let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER));
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt_layer)
|
||||
.with(redact_layer)
|
||||
.try_init();
|
||||
|
||||
// Also tee tracing output to a rotating file in the OS's
|
||||
// standard log directory so developers and beta testers can
|
||||
// hand-inspect output without launching from a terminal.
|
||||
// The path is platform-conventional:
|
||||
// * Linux: ~/.local/state/app.chanora/chanora_flutter/chanora.log
|
||||
// * macOS: ~/Library/Logs/app.chanora.chanora_flutter/chanora.log
|
||||
// * Windows: %LOCALAPPDATA%\app.chanora\chanora_flutter\logs\chanora.log
|
||||
// Best-effort: if the directory cannot be created or the
|
||||
// file cannot be opened, the layer is silently dropped and
|
||||
// stderr remains the only sink.
|
||||
let file_writer = open_log_file();
|
||||
if let Some(file) = file_writer {
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(true)
|
||||
.with_ansi(false)
|
||||
.with_writer(std::sync::Mutex::new(file));
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt_layer)
|
||||
.with(file_layer)
|
||||
.with(redact_layer)
|
||||
.try_init();
|
||||
} else {
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt_layer)
|
||||
.with(redact_layer)
|
||||
.try_init();
|
||||
}
|
||||
}
|
||||
|
||||
info!(target: "chanora_bridge", "bridge initialised");
|
||||
if let Some(p) = log_file_path() {
|
||||
info!(target: "chanora_bridge", path = %p.display(), "log file path");
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the platform-conventional log-file path as a string, or
|
||||
/// an empty string if the platform does not have one (mobile).
|
||||
#[frb(sync)]
|
||||
pub fn log_file_path_str() -> String {
|
||||
log_file_path()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Resolve the platform-conventional log-file path. Returns `None`
|
||||
/// if the platform conventions can't be honoured (e.g. neither
|
||||
/// `LOCALAPPDATA` nor `HOME` is set).
|
||||
fn log_file_path() -> Option<std::path::PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let base = std::env::var_os("LOCALAPPDATA")
|
||||
.or_else(|| std::env::var_os("APPDATA"))?;
|
||||
Some(
|
||||
std::path::PathBuf::from(base)
|
||||
.join("app.chanora")
|
||||
.join("chanora_flutter")
|
||||
.join("logs")
|
||||
.join("chanora.log"),
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var_os("HOME")?;
|
||||
Some(
|
||||
std::path::PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Logs")
|
||||
.join("app.chanora.chanora_flutter")
|
||||
.join("chanora.log"),
|
||||
)
|
||||
}
|
||||
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android"), not(target_os = "ios")))]
|
||||
{
|
||||
let base = std::env::var_os("XDG_STATE_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.or_else(|| {
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local").join("state"))
|
||||
})?;
|
||||
Some(
|
||||
base.join("app.chanora")
|
||||
.join("chanora_flutter")
|
||||
.join("chanora.log"),
|
||||
)
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the platform log file in append mode, rotating once on
|
||||
/// startup so each launch begins with a fresh file. Best-effort:
|
||||
/// returns `None` on any I/O error.
|
||||
fn open_log_file() -> Option<std::fs::File> {
|
||||
let path = log_file_path()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
// Rotate: if the existing file exceeds 4 MiB rename it to .1 so
|
||||
// we never grow unbounded. One generation is enough for
|
||||
// debugging; we are not building a real log-rotation system.
|
||||
if let Ok(meta) = std::fs::metadata(&path) {
|
||||
if meta.len() > 4 * 1024 * 1024 {
|
||||
let _ = std::fs::rename(&path, path.with_extension("log.1"));
|
||||
}
|
||||
}
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.ok()
|
||||
}
|
||||
|
||||
// ---------- DTOs ----------
|
||||
|
||||
Reference in New Issue
Block a user