// logging.rs — log-crate logger that routes by target. // // Records with target "grpc" (or any target starting with "grpc::") go // to ~/.consciousness/logs/daemon/grpc.log so we can tell gRPC events // apart from the rest of consciousness's noise. Everything else goes // to ~/.consciousness/logs/daemon/debug.log. // // Level threshold is taken from RUST_LOG (simple global level parse: // "trace"/"debug"/"info"/"warn"/"error"); defaults to "info". use std::io::Write; use std::path::PathBuf; use std::sync::Mutex; use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; fn logs_dir() -> PathBuf { dirs::home_dir().unwrap_or_default().join(".consciousness/logs/daemon") } struct RoutingLogger { grpc_file: Mutex>, debug_file: Mutex>, level: LevelFilter, } impl RoutingLogger { fn new(level: LevelFilter) -> Self { let dir = logs_dir(); let _ = std::fs::create_dir_all(&dir); let grpc = std::fs::OpenOptions::new() .create(true).append(true) .open(dir.join("grpc.log")).ok(); let debug = std::fs::OpenOptions::new() .create(true).append(true) .open(dir.join("debug.log")).ok(); Self { grpc_file: Mutex::new(grpc), debug_file: Mutex::new(debug), level, } } fn is_grpc_target(target: &str) -> bool { target == "grpc" || target.starts_with("grpc::") } } impl Log for RoutingLogger { fn enabled(&self, m: &Metadata) -> bool { // Always enable DEBUG for grpc target so the dedicated log is // actually useful without RUST_LOG wrangling; defer to the // configured level for everything else. if Self::is_grpc_target(m.target()) { return m.level() <= Level::Debug; } m.level() <= self.level } fn log(&self, record: &Record) { if !self.enabled(record.metadata()) { return; } let line = format!( "[{}] [{}] [{}] {}\n", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), record.level(), record.target(), record.args(), ); let slot = if Self::is_grpc_target(record.target()) { &self.grpc_file } else { &self.debug_file }; if let Ok(mut guard) = slot.lock() { if let Some(ref mut f) = *guard { let _ = f.write_all(line.as_bytes()); } } } fn flush(&self) { for slot in [&self.grpc_file, &self.debug_file] { if let Ok(mut g) = slot.lock() { if let Some(ref mut f) = *g { let _ = f.flush(); } } } } } fn parse_level_from_env() -> LevelFilter { let raw = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); // Parse a plain level word; if it's the module=level form, we take // the first level we find. let token = raw.split(',').next().unwrap_or("info"); let level_word = token.rsplit_once('=').map(|(_, v)| v).unwrap_or(token); match level_word.trim().to_lowercase().as_str() { "trace" => LevelFilter::Trace, "debug" => LevelFilter::Debug, "info" => LevelFilter::Info, "warn" => LevelFilter::Warn, "error" => LevelFilter::Error, "off" => LevelFilter::Off, _ => LevelFilter::Info, } } /// Install the routing logger. Safe to call at most once — subsequent /// calls return an error but are otherwise no-ops. pub fn init() -> Result<(), SetLoggerError> { let level = parse_level_from_env(); let logger = Box::new(RoutingLogger::new(level)); log::set_boxed_logger(logger)?; // Always let DEBUG records through globally so the grpc log can // capture them (the logger itself filters non-grpc targets by // `level`). The cost is that log::debug! call-sites below `level` // in other modules still do their arg formatting before being // dropped at the logger; acceptable for a debug tool. log::set_max_level(LevelFilter::Debug.max(level)); // Mark the file with a session boundary so it's easy to see where a // restart happened. log::info!( "===== consciousness logger init (level={}, pid={}) =====", level, std::process::id(), ); log::info!(target: "grpc", "===== grpc log init (level={}, pid={}) =====", level, std::process::id(), ); Ok(()) } /// Consumer of &Level so the type is used when only some callers want it. #[allow(dead_code)] pub fn current_level() -> Level { match log::max_level() { LevelFilter::Trace => Level::Trace, LevelFilter::Debug => Level::Debug, LevelFilter::Info | LevelFilter::Off => Level::Info, LevelFilter::Warn => Level::Warn, LevelFilter::Error => Level::Error, } }