Skip to content

Commit 9131a2b

Browse files
RanelkinRanel Karimov
andauthored
feat(OBS-1.a): tracing_appender file sink with daily rotation (#68) (#118)
Wires `tracing` + `tracing_appender` so the Rust backend emits JSON-line logs into `app_log_dir` with daily rotation, retaining the last 14 files. Console output is preserved via a second human-readable fmt layer to stdout. Existing `log::*` call sites are bridged into `tracing` via `tracing_log::LogTracer`, so no call-site churn. The non-blocking writer's `WorkerGuard` is held in Tauri-managed state for the app lifetime to avoid early flush/shutdown of the background writer thread. Out of scope (separate tickets): panic hook (OBS-1.b, #69) and the frontend → backend log forwarding bridge (OBS-1.c, #70). A signpost comment was added at the single TS choke point in `src/lib/logger.ts` to mark where OBS-1.c will plug in. Co-authored-by: Ranel Karimov <ranel.karimov@logscale-it.com>
1 parent fdc0287 commit 9131a2b

3 files changed

Lines changed: 108 additions & 10 deletions

File tree

src-tauri/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ aws-credential-types = "1"
2929
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt"] }
3030
log = "0.4"
3131
env_logger = "0.11"
32+
tracing = "0.1"
33+
tracing-subscriber = { version = "0.3", features = ["json", "env-filter", "fmt"] }
34+
tracing-appender = "0.2"
35+
tracing-log = "0.2"
3236
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] }
3337
sha2 = "0.10"
3438

src-tauri/src/lib.rs

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
99
use sha2::{Digest, Sha256};
1010
use tauri::{AppHandle, Manager};
1111
use tauri_plugin_sql::{Migration, MigrationKind};
12+
use tracing_appender::non_blocking::WorkerGuard;
13+
use tracing_appender::rolling::{Builder as RollingBuilder, Rotation};
14+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
1215

1316
const DB_URL: &str = "sqlite:bookie.db";
1417
const DB_FILE_NAME: &str = "bookie.db";
@@ -630,7 +633,7 @@ fn next_rand_u64() -> u64 {
630633
use std::time::{SystemTime, UNIX_EPOCH};
631634

632635
thread_local! {
633-
static STATE: Cell<u64> = Cell::new(0);
636+
static STATE: Cell<u64> = const { Cell::new(0) };
634637
}
635638

636639
STATE.with(|cell| {
@@ -969,9 +972,7 @@ async fn restore_db_backup(
969972
warn!("Sidecar missing for key={key}; aborting (no unsafe override)");
970973
return Err(BookieError::BackupSidecarMissing);
971974
}
972-
warn!(
973-
"Sidecar missing for key={key}; proceeding under user-confirmed unsafe path"
974-
);
975+
warn!("Sidecar missing for key={key}; proceeding under user-confirmed unsafe path");
975976
None
976977
} else {
977978
cleanup_tmp(&tmp_file);
@@ -993,9 +994,7 @@ async fn restore_db_backup(
993994
let actual = sha256_hex(&tmp_bytes);
994995
if actual != expected {
995996
cleanup_tmp(&tmp_file);
996-
error!(
997-
"Sidecar SHA-256 mismatch: expected={expected}, actual={actual}, key={key}"
998-
);
997+
error!("Sidecar SHA-256 mismatch: expected={expected}, actual={actual}, key={key}");
999998
return Err(BookieError::BackupSidecarMismatch);
1000999
}
10011000
info!("Sidecar SHA-256 verified for key={key}");
@@ -1479,11 +1478,70 @@ mod s3_round_trip {
14791478
}
14801479
}
14811480

1481+
/// Initialise the global `tracing` subscriber with two layers:
1482+
///
1483+
/// 1. A human-readable fmt layer that writes to stdout (so `bun run tauri dev`
1484+
/// keeps showing logs in the terminal).
1485+
/// 2. A JSON-line layer that writes to a daily-rotating file in `app_log_dir`,
1486+
/// retaining the last 14 files. The file layout produced by
1487+
/// `RollingFileAppender::builder()` is
1488+
/// `<app_log_dir>/bookie.<YYYY-MM-DD>.log`.
1489+
///
1490+
/// `RUST_LOG` controls verbosity; absent, we default to `info,bookie=debug`.
1491+
///
1492+
/// Existing `log::info!`/`log::warn!`/`log::error!` calls are bridged into
1493+
/// `tracing` via the `tracing-log` feature of `tracing-subscriber` (enabled
1494+
/// transitively by the explicit `tracing-log` dependency and by
1495+
/// `LogTracer::init()`), so adding this subscriber does not require touching
1496+
/// every call site.
1497+
///
1498+
/// Returns the `WorkerGuard` of the non-blocking file writer; the caller MUST
1499+
/// keep it alive for the lifetime of the app (we stash it in Tauri-managed
1500+
/// state) — dropping it flushes and stops the background writer thread.
1501+
fn init_tracing(log_dir: &std::path::Path) -> Result<WorkerGuard, Box<dyn std::error::Error>> {
1502+
fs::create_dir_all(log_dir)?;
1503+
1504+
let file_appender = RollingBuilder::new()
1505+
.filename_prefix("bookie")
1506+
.filename_suffix("log")
1507+
.rotation(Rotation::DAILY)
1508+
.max_log_files(14)
1509+
.build(log_dir)?;
1510+
1511+
let (nb_writer, guard) = tracing_appender::non_blocking(file_appender);
1512+
1513+
let env_filter = || {
1514+
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,bookie=debug"))
1515+
};
1516+
1517+
let stdout_layer = tracing_subscriber::fmt::layer()
1518+
.with_target(true)
1519+
.with_writer(std::io::stdout)
1520+
.with_filter(env_filter());
1521+
1522+
let file_layer = tracing_subscriber::fmt::layer()
1523+
.json()
1524+
.with_current_span(true)
1525+
.with_span_list(false)
1526+
.with_writer(nb_writer)
1527+
.with_filter(env_filter());
1528+
1529+
// Bridge classic `log` macros into `tracing` so existing call sites in this
1530+
// crate (and dependencies that emit through `log`) flow into both layers.
1531+
// Best-effort: if another component already installed a `log` logger we
1532+
// keep going rather than panicking on startup.
1533+
let _ = tracing_log::LogTracer::init();
1534+
1535+
tracing_subscriber::registry()
1536+
.with(stdout_layer)
1537+
.with(file_layer)
1538+
.try_init()?;
1539+
1540+
Ok(guard)
1541+
}
1542+
14821543
#[cfg_attr(mobile, tauri::mobile_entry_point)]
14831544
pub fn run() {
1484-
env_logger::init();
1485-
info!("Bookie starting");
1486-
14871545
tauri::Builder::default()
14881546
.plugin(
14891547
tauri_plugin_sql::Builder::new()
@@ -1492,6 +1550,39 @@ pub fn run() {
14921550
)
14931551
.plugin(tauri_plugin_opener::init())
14941552
.plugin(tauri_plugin_dialog::init())
1553+
.setup(|app| {
1554+
// OBS-1.a: install the tracing subscriber as early as possible so
1555+
// that subsequent setup work and command handlers land in the file
1556+
// sink. The panic hook (OBS-1.b) and the frontend log bridge
1557+
// (OBS-1.c) layer on top of this; they are intentionally out of
1558+
// scope here.
1559+
match app
1560+
.path()
1561+
.app_log_dir()
1562+
.map_err(|e| e.to_string())
1563+
.and_then(|dir| {
1564+
init_tracing(&dir)
1565+
.map(|guard| (dir, guard))
1566+
.map_err(|e| e.to_string())
1567+
}) {
1568+
Ok((log_dir, guard)) => {
1569+
// Hold the WorkerGuard for the lifetime of the app via
1570+
// Tauri-managed state. Dropping it flushes the non-blocking
1571+
// writer; we want that to happen at process shutdown only.
1572+
app.manage(guard);
1573+
info!("Bookie starting (log_dir={})", log_dir.display());
1574+
}
1575+
Err(err) => {
1576+
// Logger setup failed — fall back to env_logger so the app
1577+
// is still observable on stdout. We surface the failure on
1578+
// stderr because no logger is installed yet at this point.
1579+
eprintln!("tracing init failed, falling back to env_logger: {err}");
1580+
let _ = env_logger::try_init();
1581+
info!("Bookie starting (file logging disabled)");
1582+
}
1583+
}
1584+
Ok(())
1585+
})
14951586
.invoke_handler(tauri::generate_handler![
14961587
backup_database,
14971588
restore_database,

src/lib/logger.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ function createLogger(module: string) {
3434
message,
3535
safeData !== undefined ? safeData : "",
3636
);
37+
// OBS-1.c (#70) will forward `entry` to the Rust file sink (installed in
38+
// OBS-1.a) via an `invoke("log_event", entry)` IPC call placed here. Keep
39+
// this single choke point so redaction stays applied before forwarding.
3740
};
3841

3942
return {

0 commit comments

Comments
 (0)