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>"]
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-sync = [] # Feature flag for test synchronization primitives
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.38", features = ["derive"] }
|
||||
which = "7.0.3"
|
||||
@ -18,3 +22,19 @@ structopt = "0.3"
|
||||
[dev-dependencies]
|
||||
serial_test = "3.2.0"
|
||||
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 |
|
||||
| **doc** | d | run cargo doc --open --no-deps | cargo arguments |
|
||||
| **benchmark** | be | run cargo bench | cargo arguments |
|
||||
| **test*** | t | run cargo t | cargo arguments |
|
||||
| **testcapure*** | tc | run cargo t -- --nocapture | cargo arguments |
|
||||
| **test*** | t | run cargo t --features test-sync | 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) | |
|
||||
|
||||
---
|
||||
|
||||
### For tests
|
||||
|
||||
```rust
|
||||
cargo test --features test-sync
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The logger can only be initialized once per process<br>
|
||||
> Test by default runs in parallel
|
||||
|
4
justfile
4
justfile
@ -249,10 +249,10 @@ install *ARGS:
|
||||
@just make-run
|
||||
|
||||
test *ARGS:
|
||||
cargo t {{ARGS}}
|
||||
cargo t --features test-sync {{ARGS}}
|
||||
|
||||
testcapture *ARGS:
|
||||
cargo t -- --nocapture {{ARGS}}
|
||||
cargo t --features test-sync -- --nocapture {{ARGS}}
|
||||
|
||||
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_rules! log_detail {
|
||||
($level:expr, $($arg:tt)+) => {
|
||||
log::log!(
|
||||
$level,
|
||||
"[{}:{}] {}",
|
||||
file!(),
|
||||
line!(),
|
||||
format_args!($($arg)+)
|
||||
);
|
||||
log::log!($level, "{} - {}", file!(), format!($($arg)+))
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro for performance logging with timing information
|
||||
#[macro_export]
|
||||
macro_rules! log_timed {
|
||||
($level:expr, $desc:expr, $body:expr) => {{
|
||||
($level:expr, $operation:expr, $block:expr) => {{
|
||||
let start = std::time::Instant::now();
|
||||
let result = $body;
|
||||
let result = $block;
|
||||
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
|
||||
}};
|
||||
}
|
||||
|
||||
#[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};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use chrono::Local;
|
||||
use std::sync::{Mutex, Arc};
|
||||
use std::sync::OnceLock;
|
||||
use crate::error::{LogError, Result};
|
||||
pub mod prod_logger;
|
||||
#[cfg(feature = "test-sync")]
|
||||
pub mod test_logger;
|
||||
|
||||
/// Configuration for logging setup
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LogConfig {
|
||||
pub log_file: Option<PathBuf>,
|
||||
pub log_level: LevelFilter,
|
||||
pub append_log: bool,
|
||||
}
|
||||
pub use prod_logger::{LogConfig, LogWriter};
|
||||
|
||||
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: 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(feature = "test-sync")]
|
||||
pub use test_logger::init_test_logging as init_logging;
|
||||
|
||||
#[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::logging::{LogConfig, LogWriter};
|
||||
use log::LevelFilter;
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![cfg(feature = "test-sync")]
|
||||
use dir_odt_to_pdf::error::{ProcessError, Result};
|
||||
use dir_odt_to_pdf::logging::{LogConfig, init_logging};
|
||||
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::logging::{LogConfig, init_logging};
|
||||
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::logging::{LogConfig, init_logging};
|
||||
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::logging::{LogConfig, init_logging};
|
||||
use log::{LevelFilter, debug, info};
|
||||
@ -46,36 +47,17 @@ fn test_log_level_changes() -> Result<()> {
|
||||
append_log: true,
|
||||
};
|
||||
|
||||
// Try to initialize again - this should fail
|
||||
match init_logging(config) {
|
||||
Err(ProcessError::Log(LogError::AlreadyInitialized)) => {
|
||||
// Expected 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(_) => {
|
||||
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::log_timed;
|
||||
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::logging::{LogConfig, init_logging};
|
||||
use log::{LevelFilter, debug, info};
|
||||
|
Loading…
x
Reference in New Issue
Block a user