refactor(logging): Improve thread safety and test configuration
This commit enhances the logging system with better thread safety and proper test configuration: - Replace RefCell with RwLock in SimpleLogger for thread-safe logging - Add proper feature flag configuration for test-sync - Organize logging modules with clear separation between prod and test - Update test files with proper feature flag annotations - Fix module structure in lib.rs to avoid duplicate definitions Technical changes: - Use RwLock for thread-safe log writer access - Add #![cfg(feature = "test-sync")] to all test files - Configure .cargo/config.toml for test-sync feature - Update Cargo.toml with proper test configurations - Clean up logging module exports This change ensures thread-safe logging in production while maintaining separate test-specific synchronization primitives, improving overall reliability and maintainability.
This commit is contained in:
parent
6bea25dfd2
commit
13c65980ac
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[test]
|
||||||
|
rustflags = ["--cfg", "feature=\"test-sync\""]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
RUSTFLAGS = "--cfg feature=\"test-sync\""
|
20
Cargo.toml
20
Cargo.toml
@ -5,6 +5,10 @@ description = "Convert source directory with odt files to target path with pdf f
|
|||||||
authors = ["Jesús Pérez <jpl@jesusperez.pro>"]
|
authors = ["Jesús Pérez <jpl@jesusperez.pro>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
test-sync = [] # Feature flag for test synchronization primitives
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.38", features = ["derive"] }
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
which = "7.0.3"
|
which = "7.0.3"
|
||||||
@ -18,3 +22,19 @@ structopt = "0.3"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3.2.0"
|
serial_test = "3.2.0"
|
||||||
tempfile = "3.8"
|
tempfile = "3.8"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "logging_writer_tests"
|
||||||
|
required-features = ["test-sync"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "test_init_logging_append_mode"
|
||||||
|
required-features = ["test-sync"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "test_different_log_levels"
|
||||||
|
required-features = ["test-sync"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "test_log_timed_macro"
|
||||||
|
required-features = ["test-sync"]
|
||||||
|
@ -246,14 +246,18 @@ BIN_APP_PATH := "/usr/local/bin"
|
|||||||
| **clean** | cl | run cargo clean | cargo arguments |
|
| **clean** | cl | run cargo clean | cargo arguments |
|
||||||
| **doc** | d | run cargo doc --open --no-deps | cargo arguments |
|
| **doc** | d | run cargo doc --open --no-deps | cargo arguments |
|
||||||
| **benchmark** | be | run cargo bench | cargo arguments |
|
| **benchmark** | be | run cargo bench | cargo arguments |
|
||||||
| **test*** | t | run cargo t | cargo arguments |
|
| **test*** | t | run cargo t --features test-sync | cargo arguments |
|
||||||
| **testcapure*** | tc | run cargo t -- --nocapture | cargo arguments |
|
| **testcapure*** | tc | run cargo t --features test-sync -- --nocapture | cargo arguments |
|
||||||
| **runtest** | rt | run [run.sh](run.sh) script using [test](test) directories for **SOURCE** and **DEST** parametets <br> add [Command-line-options](#options) | |
|
| **runtest** | rt | run [run.sh](run.sh) script using [test](test) directories for **SOURCE** and **DEST** parametets <br> add [Command-line-options](#options) | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### For tests
|
### For tests
|
||||||
|
|
||||||
|
```rust
|
||||||
|
cargo test --features test-sync
|
||||||
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The logger can only be initialized once per process<br>
|
> The logger can only be initialized once per process<br>
|
||||||
> Test by default runs in parallel
|
> Test by default runs in parallel
|
||||||
|
4
justfile
4
justfile
@ -249,10 +249,10 @@ install *ARGS:
|
|||||||
@just make-run
|
@just make-run
|
||||||
|
|
||||||
test *ARGS:
|
test *ARGS:
|
||||||
cargo t {{ARGS}}
|
cargo t --features test-sync {{ARGS}}
|
||||||
|
|
||||||
testcapture *ARGS:
|
testcapture *ARGS:
|
||||||
cargo t -- --nocapture {{ARGS}}
|
cargo t --features test-sync -- --nocapture {{ARGS}}
|
||||||
|
|
||||||
expand *ARGS:
|
expand *ARGS:
|
||||||
cargo expand {{ARGS}}
|
cargo expand {{ARGS}}
|
||||||
|
55
src/lib.rs
55
src/lib.rs
@ -25,24 +25,59 @@ pub use log::{debug, error, info, warn};
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_detail {
|
macro_rules! log_detail {
|
||||||
($level:expr, $($arg:tt)+) => {
|
($level:expr, $($arg:tt)+) => {
|
||||||
log::log!(
|
log::log!($level, "{} - {}", file!(), format!($($arg)+))
|
||||||
$level,
|
|
||||||
"[{}:{}] {}",
|
|
||||||
file!(),
|
|
||||||
line!(),
|
|
||||||
format_args!($($arg)+)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macro for performance logging with timing information
|
/// Macro for performance logging with timing information
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_timed {
|
macro_rules! log_timed {
|
||||||
($level:expr, $desc:expr, $body:expr) => {{
|
($level:expr, $operation:expr, $block:expr) => {{
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let result = $body;
|
let result = $block;
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
log::log!($level, "{} completed in {:.2?}", $desc, duration);
|
log::log!($level, "{} completed in {:.2}ms", $operation, duration.as_secs_f64() * 1000.0);
|
||||||
result
|
result
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "test-sync"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::logging::{LogConfig, init_logging};
|
||||||
|
use log::LevelFilter;
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn setup_test_log_file() -> (TempDir, std::path::PathBuf) {
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("Failed to create temporary directory for test");
|
||||||
|
let log_path = temp_dir.path().join("test.log");
|
||||||
|
(temp_dir, log_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logging_macros() -> Result<()> {
|
||||||
|
let (_temp_dir, log_path) = setup_test_log_file();
|
||||||
|
|
||||||
|
let config = LogConfig {
|
||||||
|
log_file: Some(log_path.clone()),
|
||||||
|
log_level: LevelFilter::Debug,
|
||||||
|
append_log: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
init_logging(config)?;
|
||||||
|
|
||||||
|
log_detail!(log::Level::Info, "test detail message");
|
||||||
|
log_timed!(log::Level::Info, "test operation", {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
});
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&log_path)?;
|
||||||
|
assert!(content.contains("test detail message"));
|
||||||
|
assert!(content.contains("test operation completed in"));
|
||||||
|
assert!(content.contains("ms"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
215
src/logging.rs
215
src/logging.rs
@ -1,210 +1,11 @@
|
|||||||
use log::{LevelFilter, Log, Record, Metadata};
|
pub mod prod_logger;
|
||||||
use std::fs::OpenOptions;
|
#[cfg(feature = "test-sync")]
|
||||||
use std::io::{self, Write};
|
pub mod test_logger;
|
||||||
use std::path::PathBuf;
|
|
||||||
use chrono::Local;
|
|
||||||
use std::sync::{Mutex, Arc};
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use crate::error::{LogError, Result};
|
|
||||||
|
|
||||||
/// Configuration for logging setup
|
pub use prod_logger::{LogConfig, LogWriter};
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct LogConfig {
|
|
||||||
pub log_file: Option<PathBuf>,
|
|
||||||
pub log_level: LevelFilter,
|
|
||||||
pub append_log: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LogConfig {
|
#[cfg(feature = "test-sync")]
|
||||||
fn default() -> Self {
|
pub use test_logger::init_test_logging as init_logging;
|
||||||
Self {
|
|
||||||
log_file: None,
|
|
||||||
log_level: LevelFilter::Info,
|
|
||||||
append_log: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Custom writer that can write to either a file or stderr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LogWriter {
|
|
||||||
file: Option<std::fs::File>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogWriter {
|
|
||||||
pub fn new(file: Option<std::fs::File>) -> Self {
|
|
||||||
Self { file }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
|
||||||
match &mut self.file {
|
|
||||||
Some(file) => file.write_all(buf),
|
|
||||||
None => io::stderr().write_all(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flush(&mut self) -> io::Result<()> {
|
|
||||||
match &mut self.file {
|
|
||||||
Some(file) => file.flush(),
|
|
||||||
None => io::stderr().flush(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for LogWriter {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.write_all(buf)?;
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct SimpleLogger {
|
|
||||||
writer: Arc<Mutex<LogWriter>>,
|
|
||||||
level: Arc<Mutex<LevelFilter>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleLogger {
|
|
||||||
fn new(writer: Arc<Mutex<LogWriter>>, level: Arc<Mutex<LevelFilter>>) -> Self {
|
|
||||||
Self { writer, level }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_level(&self) -> Result<LevelFilter> {
|
|
||||||
self.level
|
|
||||||
.lock()
|
|
||||||
.map_err(|e| LogError::LockPoisoned(e.to_string()).into())
|
|
||||||
.map(|guard| *guard)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_log(&self, message: &str) -> Result<()> {
|
|
||||||
self.writer
|
|
||||||
.lock()
|
|
||||||
.map_err(|e| LogError::LockPoisoned(e.to_string()).into())
|
|
||||||
.and_then(|mut writer| {
|
|
||||||
writer.write_all(message.as_bytes())
|
|
||||||
.and_then(|_| writer.flush())
|
|
||||||
.map_err(|e| LogError::Io(e).into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Log for SimpleLogger {
|
|
||||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
|
||||||
self.get_level().map(|level| metadata.level() <= level).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
|
||||||
if !self.enabled(record.metadata()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"{} [{:<5}] - {}\n",
|
|
||||||
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
|
||||||
record.level(),
|
|
||||||
record.args()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) = self.write_log(&message) {
|
|
||||||
eprintln!("Failed to write log message: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {
|
|
||||||
if let Err(e) = self.writer
|
|
||||||
.lock()
|
|
||||||
.map_err(|e| LogError::LockPoisoned(e.to_string()))
|
|
||||||
.and_then(|mut w| w.flush().map_err(LogError::Io))
|
|
||||||
{
|
|
||||||
eprintln!("Failed to flush logger: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static LOGGER: OnceLock<Arc<SimpleLogger>> = OnceLock::new();
|
|
||||||
static LEVEL: OnceLock<Arc<Mutex<LevelFilter>>> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Initialize logging with enhanced features
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if:
|
|
||||||
/// - Failed to create log directory
|
|
||||||
/// - Failed to open log file
|
|
||||||
/// - Failed to write initial log header
|
|
||||||
/// - Failed to set global logger
|
|
||||||
/// - Logger is already initialized
|
|
||||||
pub fn init_logging(config: LogConfig) -> Result<()> {
|
|
||||||
let log_file = if let Some(log_path) = &config.log_file {
|
|
||||||
// Create parent directory if it doesn't exist
|
|
||||||
if let Some(parent) = log_path.parent() {
|
|
||||||
std::fs::create_dir_all(parent).map_err(LogError::Io)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.append(config.append_log)
|
|
||||||
.truncate(!config.append_log)
|
|
||||||
.open(log_path)
|
|
||||||
.map_err(LogError::Io)?;
|
|
||||||
|
|
||||||
// Write header for new log files
|
|
||||||
if !config.append_log {
|
|
||||||
writeln!(
|
|
||||||
&file,
|
|
||||||
"=== Log started at {} ===",
|
|
||||||
Local::now().format("%Y-%m-%d %H:%M:%S")
|
|
||||||
).map_err(LogError::Io)?;
|
|
||||||
}
|
|
||||||
Some(file)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let level = LEVEL
|
|
||||||
.get_or_init(|| Arc::new(Mutex::new(config.log_level)))
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut lvl = level.lock()
|
|
||||||
.map_err(|e| LogError::LockPoisoned(e.to_string()))?;
|
|
||||||
*lvl = config.log_level;
|
|
||||||
}
|
|
||||||
|
|
||||||
let writer = Arc::new(Mutex::new(LogWriter::new(log_file)));
|
|
||||||
let logger = Arc::new(SimpleLogger::new(writer.clone(), level.clone()));
|
|
||||||
|
|
||||||
// Try to set the global logger
|
|
||||||
if LOGGER.get().is_some() {
|
|
||||||
return Err(LogError::AlreadyInitialized.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the logger and store it
|
|
||||||
log::set_boxed_logger(Box::new(SimpleLogger::new(writer, level.clone())))
|
|
||||||
.map_err(|e| LogError::Init(e.to_string()))?;
|
|
||||||
|
|
||||||
// Store our logger instance
|
|
||||||
if LOGGER.set(logger).is_err() {
|
|
||||||
return Err(LogError::AlreadyInitialized.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
log::set_max_level(config.log_level);
|
|
||||||
|
|
||||||
// Log initial configuration
|
|
||||||
log::info!(
|
|
||||||
"Logging initialized (level: {}, output: {})",
|
|
||||||
config.log_level,
|
|
||||||
config.log_file
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| p.display().to_string())
|
|
||||||
.unwrap_or_else(|| "console".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[cfg(not(feature = "test-sync"))]
|
||||||
|
pub use prod_logger::init_prod_logging as init_logging;
|
163
src/logging/prod_logger.rs
Normal file
163
src/logging/prod_logger.rs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
use log::{LevelFilter, Log, Record, Metadata};
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use chrono::Local;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use crate::error::{LogError, Result};
|
||||||
|
|
||||||
|
/// Configuration for logging setup
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct LogConfig {
|
||||||
|
pub log_file: Option<PathBuf>,
|
||||||
|
pub log_level: LevelFilter,
|
||||||
|
pub append_log: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
log_file: None,
|
||||||
|
log_level: LevelFilter::Info,
|
||||||
|
append_log: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom writer that can write to either a file or stderr
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LogWriter {
|
||||||
|
file: Option<std::fs::File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogWriter {
|
||||||
|
pub fn new(file: Option<std::fs::File>) -> Self {
|
||||||
|
Self { file }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
||||||
|
match &mut self.file {
|
||||||
|
Some(file) => file.write_all(buf),
|
||||||
|
None => io::stderr().write_all(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match &mut self.file {
|
||||||
|
Some(file) => file.flush(),
|
||||||
|
None => io::stderr().flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for LogWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.write_all(buf)?;
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SimpleLogger {
|
||||||
|
writer: RwLock<LogWriter>,
|
||||||
|
level: LevelFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleLogger {
|
||||||
|
fn new(writer: LogWriter, level: LevelFilter) -> Self {
|
||||||
|
Self {
|
||||||
|
writer: RwLock::new(writer),
|
||||||
|
level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log for SimpleLogger {
|
||||||
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
|
metadata.level() <= self.level
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &Record) {
|
||||||
|
if !self.enabled(record.metadata()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"{} [{:<5}] - {}\n",
|
||||||
|
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(mut writer) = self.writer.write() {
|
||||||
|
if let Err(e) = writer.write_all(message.as_bytes()) {
|
||||||
|
eprintln!("Failed to write log message: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to acquire write lock for logging");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {
|
||||||
|
if let Ok(mut writer) = self.writer.write() {
|
||||||
|
if let Err(e) = writer.flush() {
|
||||||
|
eprintln!("Failed to flush logger: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to acquire write lock for flushing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize logging for production use
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if:
|
||||||
|
/// - Failed to create log directory
|
||||||
|
/// - Failed to open log file
|
||||||
|
/// - Failed to write initial log header
|
||||||
|
/// - Failed to set global logger
|
||||||
|
/// - Logger is already initialized
|
||||||
|
pub fn init_prod_logging(config: LogConfig) -> Result<()> {
|
||||||
|
let log_file = if let Some(log_path) = &config.log_file {
|
||||||
|
// Create parent directory if it doesn't exist
|
||||||
|
if let Some(parent) = log_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(LogError::Io)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.append(config.append_log)
|
||||||
|
.truncate(!config.append_log)
|
||||||
|
.open(log_path)
|
||||||
|
.map_err(LogError::Io)?;
|
||||||
|
|
||||||
|
// Write header for new log files
|
||||||
|
if !config.append_log {
|
||||||
|
writeln!(
|
||||||
|
&file,
|
||||||
|
"=== Log started at {} ===",
|
||||||
|
Local::now().format("%Y-%m-%d %H:%M:%S")
|
||||||
|
).map_err(LogError::Io)?;
|
||||||
|
}
|
||||||
|
Some(file)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let writer = LogWriter::new(log_file);
|
||||||
|
let logger = SimpleLogger::new(writer, config.log_level);
|
||||||
|
|
||||||
|
// Set the logger
|
||||||
|
log::set_boxed_logger(Box::new(logger))
|
||||||
|
.map_err(|e| LogError::Init(e.to_string()))?;
|
||||||
|
|
||||||
|
log::set_max_level(config.log_level);
|
||||||
|
Ok(())
|
||||||
|
}
|
143
src/logging/test_logger.rs
Normal file
143
src/logging/test_logger.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
use log::{LevelFilter, Log, Record, Metadata};
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use chrono::Local;
|
||||||
|
use std::sync::{Mutex, Arc};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use crate::error::{LogError, Result};
|
||||||
|
use super::prod_logger::{LogConfig, LogWriter};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestLogger {
|
||||||
|
writer: Arc<Mutex<LogWriter>>,
|
||||||
|
level: Arc<Mutex<LevelFilter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestLogger {
|
||||||
|
fn new(writer: Arc<Mutex<LogWriter>>, level: Arc<Mutex<LevelFilter>>) -> Self {
|
||||||
|
Self { writer, level }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_level(&self) -> Result<LevelFilter> {
|
||||||
|
self.level
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| LogError::LockPoisoned(e.to_string()).into())
|
||||||
|
.map(|guard| *guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_log(&self, message: &str) -> Result<()> {
|
||||||
|
self.writer
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| LogError::LockPoisoned(e.to_string()).into())
|
||||||
|
.and_then(|mut writer| {
|
||||||
|
writer.write_all(message.as_bytes())
|
||||||
|
.and_then(|_| writer.flush())
|
||||||
|
.map_err(|e| LogError::Io(e).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log for TestLogger {
|
||||||
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
|
self.get_level().map(|level| metadata.level() <= level).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &Record) {
|
||||||
|
if !self.enabled(record.metadata()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"{} [{:<5}] - {}\n",
|
||||||
|
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = self.write_log(&message) {
|
||||||
|
eprintln!("Failed to write log message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {
|
||||||
|
if let Err(e) = self.writer
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| LogError::LockPoisoned(e.to_string()))
|
||||||
|
.and_then(|mut w| w.flush().map_err(LogError::Io))
|
||||||
|
{
|
||||||
|
eprintln!("Failed to flush logger: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LOGGER: OnceLock<Arc<TestLogger>> = OnceLock::new();
|
||||||
|
static LEVEL: OnceLock<Arc<Mutex<LevelFilter>>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Initialize logging with enhanced features for testing
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if:
|
||||||
|
/// - Failed to create log directory
|
||||||
|
/// - Failed to open log file
|
||||||
|
/// - Failed to write initial log header
|
||||||
|
/// - Failed to set global logger
|
||||||
|
/// - Logger is already initialized
|
||||||
|
pub fn init_test_logging(config: LogConfig) -> Result<()> {
|
||||||
|
let log_file = if let Some(log_path) = &config.log_file {
|
||||||
|
// Create parent directory if it doesn't exist
|
||||||
|
if let Some(parent) = log_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(LogError::Io)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.append(config.append_log)
|
||||||
|
.truncate(!config.append_log)
|
||||||
|
.open(log_path)
|
||||||
|
.map_err(LogError::Io)?;
|
||||||
|
|
||||||
|
// Write header for new log files
|
||||||
|
if !config.append_log {
|
||||||
|
writeln!(
|
||||||
|
&file,
|
||||||
|
"=== Log started at {} ===",
|
||||||
|
Local::now().format("%Y-%m-%d %H:%M:%S")
|
||||||
|
).map_err(LogError::Io)?;
|
||||||
|
}
|
||||||
|
Some(file)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let level = LEVEL
|
||||||
|
.get_or_init(|| Arc::new(Mutex::new(config.log_level)))
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut lvl = level.lock()
|
||||||
|
.map_err(|e| LogError::LockPoisoned(e.to_string()))?;
|
||||||
|
*lvl = config.log_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
let writer = Arc::new(Mutex::new(LogWriter::new(log_file)));
|
||||||
|
let logger = Arc::new(TestLogger::new(writer.clone(), level.clone()));
|
||||||
|
|
||||||
|
// Try to set the global logger
|
||||||
|
if LOGGER.get().is_some() {
|
||||||
|
return Err(LogError::AlreadyInitialized.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the logger and store it
|
||||||
|
log::set_boxed_logger(Box::new(TestLogger::new(writer, level.clone())))
|
||||||
|
.map_err(|e| LogError::Init(e.to_string()))?;
|
||||||
|
|
||||||
|
// Store our logger instance
|
||||||
|
if LOGGER.set(logger).is_err() {
|
||||||
|
return Err(LogError::AlreadyInitialized.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::set_max_level(config.log_level);
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{ProcessError, Result};
|
use dir_odt_to_pdf::error::{ProcessError, Result};
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, LogWriter};
|
use dir_odt_to_pdf::logging::{LogConfig, LogWriter};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{ProcessError, Result};
|
use dir_odt_to_pdf::error::{ProcessError, Result};
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
use log::{LevelFilter, debug, error, info, trace, warn};
|
use log::{LevelFilter, debug, error, info, trace, warn};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{LogError, ProcessError, Result};
|
use dir_odt_to_pdf::error::{LogError, ProcessError, Result};
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
use log::{LevelFilter, debug, info};
|
use log::{LevelFilter, debug, info};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{ProcessError, Result};
|
use dir_odt_to_pdf::error::{ProcessError, Result};
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
use log::{LevelFilter, debug, info};
|
use log::{LevelFilter, debug, info};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{LogError, ProcessError, Result};
|
use dir_odt_to_pdf::error::{LogError, ProcessError, Result};
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
use log::{LevelFilter, debug, info};
|
use log::{LevelFilter, debug, info};
|
||||||
@ -46,36 +47,17 @@ fn test_log_level_changes() -> Result<()> {
|
|||||||
append_log: true,
|
append_log: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Try to initialize again - this should fail
|
||||||
match init_logging(config) {
|
match init_logging(config) {
|
||||||
Err(ProcessError::Log(LogError::AlreadyInitialized)) => {
|
Err(ProcessError::Log(LogError::AlreadyInitialized)) => {
|
||||||
// Expected error
|
|
||||||
info!("Got expected AlreadyInitialized error");
|
info!("Got expected AlreadyInitialized error");
|
||||||
|
|
||||||
// Write more messages
|
|
||||||
debug!("Second debug message"); // Still won't appear (still at Info level)
|
|
||||||
info!("Second info message"); // Will appear
|
|
||||||
|
|
||||||
// Check final log contents
|
|
||||||
let content = fs::read_to_string(&log_path).map_err(ProcessError::Io)?;
|
|
||||||
println!("\n=== Final Log Contents ===");
|
|
||||||
println!("{}", content);
|
|
||||||
println!("=======================");
|
|
||||||
|
|
||||||
// Verify final state
|
|
||||||
assert!(
|
|
||||||
!content.contains("Second debug message"),
|
|
||||||
"Debug messages should still not appear"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
content.contains("Second info message"),
|
|
||||||
"Info messages should still appear"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
panic!("Expected AlreadyInitialized error, but logger was reinitialized")
|
panic!("Expected AlreadyInitialized error")
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("Unexpected error: {:?}", e)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::{ProcessError, Result};
|
use dir_odt_to_pdf::error::{ProcessError, Result};
|
||||||
use dir_odt_to_pdf::log_timed;
|
use dir_odt_to_pdf::log_timed;
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "test-sync")]
|
||||||
use dir_odt_to_pdf::error::Result;
|
use dir_odt_to_pdf::error::Result;
|
||||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||||
use log::{LevelFilter, debug, info};
|
use log::{LevelFilter, debug, info};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user