use crossbeam::channel::{Sender, unbounded}; use std::{ fmt, fs::OpenOptions, io::{BufWriter, Write}, path::{Path, PathBuf}, str::FromStr, sync::{Arc, RwLock}, time::SystemTime, }; use tracing::{Event, Level, Subscriber, info}; use tracing_subscriber::{ EnvFilter, layer::{Context, Layer, SubscriberExt}, registry::LookupSpan, util::SubscriberInitExt, }; #[derive(Debug, Default)] struct App; #[derive(Debug, Clone)] pub struct LogEntry { timestamp: SystemTime, level: Level, message: String, } struct BufferLayer { log_entries: Arc>>, sender: Sender, } impl Layer for BufferLayer where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { let metadata = event.metadata(); let level = *metadata.level(); let timestamp = SystemTime::now(); let mut message = String::new(); let mut visitor = LogVisitor::new(&mut message); event.record(&mut visitor); let log_entry = LogEntry { timestamp, level, message, }; if let Ok(mut guard) = self.log_entries.write() { guard.push(log_entry.clone()); } let _ = self.sender.send(log_entry); } } struct LogVisitor<'msg> { message: &'msg mut String, } impl<'msg> LogVisitor<'msg> { fn new(message: &'msg mut String) -> Self { Self { message } } } impl<'msg> tracing::field::Visit for LogVisitor<'msg> { fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn fmt::Debug) { use std::fmt::Write as _; if field.name() == "message" { write!(self.message, "{:?}", value).unwrap(); } else { write!(self.message, "{}={:?} ", field.name(), value).unwrap(); } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum LogLevel { Error, #[default] Info, Debug, Trace, } impl fmt::Display for LogLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } } impl FromStr for LogLevel { type Err = (); fn from_str(s: &str) -> Result { match s { "trace" => Ok(Self::Trace), "debug" => Ok(Self::Debug), "info" => Ok(Self::Info), "error" => Ok(Self::Error), _ => Err(()), } } } #[derive(Debug, Clone,PartialEq,Eq,PartialOrd)] pub struct LoggerConfig { pub log_level: LogLevel, pub log_to_file: bool, pub log_file_path: PathBuf, pub log_to_stdout: bool, pub stdout_color: bool, pub stdout_include_time: bool, pub file_include_time: bool, } impl LoggerConfig { pub fn level(mut self, level: LogLevel) -> Self { self.log_level = level; self } pub fn log_to_file(mut self, log_to_file: bool) -> Self { self.log_to_file = log_to_file; self } pub fn colored_stdout(mut self, show_color: bool) -> Self { self.stdout_color = show_color; self } pub fn log_to_stdout(mut self, stdout: bool) -> Self { self.log_to_stdout = stdout; self } pub fn log_path>(mut self, path: P) -> Self { self.log_file_path = path.as_ref().to_path_buf(); self } pub fn stdout_include_time(mut self, include: bool) -> Self { self.stdout_include_time = include; self } pub fn file_include_time(mut self, include: bool) -> Self { self.file_include_time = include; self } } impl Default for LoggerConfig { fn default() -> Self { Self { log_level: LogLevel::Debug, log_to_file: true, log_file_path: "zenyx.log".into(), log_to_stdout: true, stdout_color: true, stdout_include_time: false, file_include_time: false, } } } #[derive(Debug, Clone,Copy, PartialEq,Eq,PartialOrd)] pub enum LogQuery { All, From(SystemTime), } #[derive(Debug, Clone)] pub struct Logger { config: LoggerConfig, log_entries: Arc>>, _sender: Sender, } impl Logger { pub fn new(config: LoggerConfig) -> Self { let (sender, receiver) = unbounded(); let log_entries = Arc::new(RwLock::new(Vec::new())); if config.log_to_stdout { let stdout_receiver = receiver.clone(); let stdout_color = config.stdout_color; let stdout_include_time = config.stdout_include_time; std::thread::spawn(move || { for entry in stdout_receiver { let line = format_entry(&entry, stdout_color, stdout_include_time); println!("{}", line); } }); } if config.log_to_file { let file_receiver = receiver.clone(); let log_file_path = config.log_file_path.clone(); let file_include_time = config.file_include_time; std::thread::spawn(move || { let file = OpenOptions::new() .append(true) .create(true) .open(&log_file_path) .unwrap_or_else(|_| { OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&log_file_path) .expect("Failed to create log file") }); let mut writer = BufWriter::new(file); for entry in file_receiver { let line = format_entry(&entry, false, file_include_time); writer .write_all(format!("{}\n", line).as_bytes()) .expect("Failed to write to log file"); writer.flush().expect("Failed to flush log file"); } }); } let buffer_layer = BufferLayer { log_entries: log_entries.clone(), sender: sender.clone(), }; let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { format!("{}={}", env!("CARGO_CRATE_NAME"), config.log_level).into() }); let subscriber = tracing_subscriber::registry() .with(env_filter) .with(buffer_layer); subscriber.init(); Self { config, log_entries, _sender: sender, } } pub fn get_logs(&self, log_query: LogQuery) -> Vec { let guard = self.log_entries.read().unwrap(); match log_query { LogQuery::All => guard.clone(), LogQuery::From(time) => guard .iter() .filter(|e| e.timestamp >= time) .cloned() .collect(), } } } fn format_entry(entry: &LogEntry, use_color: bool, include_time: bool) -> String { let timestamp = if include_time { format!( "[{}] ", humantime::format_rfc3339_seconds(entry.timestamp) .to_string() .trim_end_matches('Z') .replace('T', " ") ) } else { String::new() }; let level = if use_color { match entry.level { Level::ERROR => format!("\x1b[31m{}\x1b[0m", entry.level), Level::WARN => format!("\x1b[33m{}\x1b[0m", entry.level), Level::INFO => format!("\x1b[32m{}\x1b[0m", entry.level), Level::DEBUG => format!("\x1b[36m{}\x1b[0m", entry.level), Level::TRACE => format!("\x1b[34m{}\x1b[0m", entry.level), } } else { entry.level.to_string() }; format!("{}{} {}", timestamp, level, entry.message) } fn main() -> Result<(), Box> { let logger_config = LoggerConfig::default() .colored_stdout(true) .level(LogLevel::Debug) .log_to_file(true) .stdout_include_time(false) .file_include_time(true) .log_path(format!("{}.log", env!("CARGO_PKG_NAME"))); let logger = Logger::new(logger_config); let app = App::default(); tracing::info!("Application initialized"); tracing::error!("Another log message with value: {}", 42); let logs = logger.get_logs(LogQuery::All); println!("\n--- All Logs ---"); for entry in logs { println!("{}", format_entry(&entry, false, true)); } let now = SystemTime::now(); tracing::warn!("This is a warning after the initial logs"); let new_logs = logger.get_logs(LogQuery::From(now)); println!("\n--- Logs Since Now ---"); for entry in new_logs { println!("{}", format_entry(&entry, false, true)); } info!("Application finished"); // logger.get_logs(); Ok(()) }