use chrono::Utc; use chrono_tz::Tz; use std::path::PathBuf; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{ EnvFilter, fmt, fmt::time::FormatTime, layer::SubscriberExt, util::SubscriberInitExt, }; #[derive(Clone, Copy, Debug)] struct ConfiguredTimestamp { timezone: Tz, } impl FormatTime for ConfiguredTimestamp { fn format_time( &self, writer: &mut tracing_subscriber::fmt::format::Writer<'_>, ) -> std::fmt::Result { write!( writer, "{}", Utc::now() .with_timezone(&self.timezone) .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) ) } } /// Get the default log directory path: ~/.picobot/logs pub fn get_default_log_dir() -> PathBuf { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); home.join(".picobot").join("logs") } /// Get the default config file path: ~/.picobot/config.json pub fn get_default_config_path() -> PathBuf { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); home.join(".picobot").join("config.json") } /// Initialize logging with file appender /// Logs are written to ~/.picobot/logs/ with daily rotation pub fn init_logging(timezone: Tz) { let log_dir = get_default_log_dir(); // Create log directory if it doesn't exist if !log_dir.exists() { if let Err(e) = std::fs::create_dir_all(&log_dir) { eprintln!( "Warning: Failed to create log directory {}: {}", log_dir.display(), e ); } } // Create file appender with daily rotation let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, "picobot.log"); // Build subscriber with both console and file output let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let file_layer = fmt::layer() .with_writer(file_appender) .with_timer(ConfiguredTimestamp { timezone }) .with_ansi(false) .with_target(true) .with_level(true) .with_thread_ids(true); let console_layer = fmt::layer() .with_timer(ConfiguredTimestamp { timezone }) .with_target(true) .with_level(true); tracing_subscriber::registry() .with(env_filter) .with(console_layer) .with(file_layer) .init(); tracing::info!("Logging initialized. Log directory: {}", log_dir.display()); } /// Initialize logging without file output (console only) pub fn init_logging_console_only(timezone: Tz) { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let console_layer = fmt::layer() .with_timer(ConfiguredTimestamp { timezone }) .with_target(true) .with_level(true); tracing_subscriber::registry() .with(env_filter) .with(console_layer) .init(); tracing::info!("Logging initialized (console only)"); }