From 00d112606ec4eed1d875e0a79ac78014c8f38d33 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 23 Jan 2026 14:44:31 -0800 Subject: [PATCH 1/2] [update] the Log file handler use a mutex for keeping the file open. --- crates/lambda-rs-logging/src/handler.rs | 44 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/lambda-rs-logging/src/handler.rs b/crates/lambda-rs-logging/src/handler.rs index 82903c77..554767bd 100644 --- a/crates/lambda-rs-logging/src/handler.rs +++ b/crates/lambda-rs-logging/src/handler.rs @@ -1,7 +1,10 @@ //! Log handling implementations for the logger. use std::{ - fs::OpenOptions, + fs::{ + File, + OpenOptions, + }, io::{ self, IsTerminal, @@ -25,15 +28,21 @@ pub trait Handler: Send + Sync { /// A handler that logs to a file. #[derive(Debug)] pub struct FileHandler { - file: String, log_buffer: Mutex>, + writer: Mutex>, } impl FileHandler { - pub fn new(file: String) -> Self { + pub fn new(path: String) -> Self { + let log_file = OpenOptions::new() + .append(true) + .create(true) + .open(&path) + .expect("open log file"); + Self { - file, log_buffer: Mutex::new(Vec::new()), + writer: Mutex::new(io::BufWriter::new(log_file)), } } } @@ -59,27 +68,30 @@ impl Handler for FileHandler { LogLevel::FATAL => format!("\x1B[31;1m{}\x1B[0m", log_message), }; - let mut buf = self.log_buffer.lock().unwrap(); - buf.push(colored_message); + let mut buffer = match self.log_buffer.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + buffer.push(colored_message); // Flush buffer every ten messages. - if buf.len() < 10 { + if buffer.len() < 10 { return; } - let log_message = buf.join("\n"); + let messages = std::mem::take(&mut *buffer); + drop(buffer); - let mut file = OpenOptions::new() - .append(true) - .create(true) - .open(self.file.clone()) - .unwrap(); + let log_message = messages.join("\n"); - file + let mut writer = match self.writer.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + writer .write_all(log_message.as_bytes()) .expect("Unable to write data"); - - buf.clear(); + let _ = writer.flush(); } } From 88f181291f3a5303d950fa8e2e236cf337c8e2d6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 23 Jan 2026 14:54:50 -0800 Subject: [PATCH 2/2] [add] tests. --- crates/lambda-rs-logging/src/lib.rs | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/crates/lambda-rs-logging/src/lib.rs b/crates/lambda-rs-logging/src/lib.rs index c760e78d..98b1bb9c 100644 --- a/crates/lambda-rs-logging/src/lib.rs +++ b/crates/lambda-rs-logging/src/lib.rs @@ -502,6 +502,51 @@ mod tests { assert!(!content.is_empty()); } + #[test] + #[cfg(unix)] + fn file_handler_does_not_reopen_between_flushes() { + use std::{ + fs, + time::UNIX_EPOCH, + }; + + let tmp = std::env::temp_dir(); + let base = format!( + "lambda_logging_persist_{}_{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let original = tmp.join(format!("{}.log", base)); + let moved = tmp.join(format!("{}_moved.log", base)); + + let logger = Logger::new(LogLevel::TRACE, "file_reopen"); + logger.add_handler(Box::new(crate::handler::FileHandler::new( + original.to_string_lossy().to_string(), + ))); + + for i in 0..10 { + logger.info(format!("batch1_line{}", i)); + } + + fs::rename(&original, &moved).expect("rename log file while handler lives"); + + for i in 0..10 { + logger.info(format!("batch2_line{}", i)); + } + + assert!( + !original.exists(), + "handler should not reopen and recreate the original path" + ); + + let content = fs::read_to_string(&moved).expect("moved file must exist"); + assert!(content.contains("batch1_line0")); + assert!(content.contains("batch2_line0")); + } + #[test] fn macro_early_guard_avoids_formatting() { // Ensure TRACE is disabled by setting level to INFO.