@@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
99use sha2:: { Digest , Sha256 } ;
1010use tauri:: { AppHandle , Manager } ;
1111use 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
1316const DB_URL : & str = "sqlite:bookie.db" ;
1417const 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) ]
14831544pub 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,
0 commit comments