use crate::profiling::{get_debug_level, DebugLevel, ProfilePaths};
use chrono::Local;
use parking_lot::Mutex;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use thag_common::static_lazy;
#[cfg(feature = "full_profiling")]
use crate::safe_alloc;
static_lazy! {
DebugLogger: Option<Mutex<BufWriter<File>>> = {
#[cfg(feature = "full_profiling")]
{
safe_alloc! {
create_debug_logger()
}
}
#[cfg(not(feature = "full_profiling"))]
{
create_debug_logger()
}
}
}
fn create_debug_logger() -> Option<Mutex<BufWriter<File>>> {
let debug_level = get_debug_level();
if debug_level != DebugLevel::None {
let log_path = PathBuf::from(&ProfilePaths::get().debug_log);
if debug_level == DebugLevel::Announce {
eprintln!("Thag Profiler debug log: {}", log_path.display());
}
if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_path) {
let mut header_writer = BufWriter::with_capacity(65536, file);
let _ = writeln!(header_writer, "--- Thag Profiler Debug Log ---");
let _ = writeln!(
header_writer,
"Executable: {}",
ProfilePaths::get().executable_stem
);
let _ = writeln!(
header_writer,
"Timestamp: {}",
ProfilePaths::get().timestamp
);
let _ = writeln!(header_writer, "Debug level: {debug_level}");
let _ = writeln!(
header_writer,
"Start time: {}",
Local::now().format("%Y-%m-%d %H:%M:%S")
);
let _ = writeln!(header_writer, "{}", "─".repeat(27));
if let Err(e) = header_writer.flush() {
eprintln!("Failed to flush debug log header: {e}");
}
return Some(Mutex::new(header_writer));
}
}
None
}
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn get_debug_log_path() -> Option<String> {
#[cfg(feature = "full_profiling")]
{
safe_alloc! {
if get_debug_level() == DebugLevel::None {
None
} else {
Some(ProfilePaths::get().debug_log.clone())
}
}
}
#[cfg(not(feature = "full_profiling"))]
{
if get_debug_level() == DebugLevel::None {
None
} else {
Some(ProfilePaths::get().debug_log.clone())
}
}
}
#[allow(clippy::missing_const_for_fn)]
pub fn flush_debug_log() {
#[cfg(not(feature = "full_profiling"))]
{
if let Some(logger) = DebugLogger::get() {
let flush_result = {
let mut locked_writer = logger.lock();
locked_writer.flush()
};
if let Err(e) = flush_result {
eprintln!("Error flushing debug log: {e}");
}
}
}
#[cfg(feature = "full_profiling")]
{}
}
#[cfg(feature = "debug_logging")]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {
$crate::safe_alloc! {
if let Some(logger) = $crate::DebugLogger::get() {
use std::io::Write;
let _write_result = {
let mut locked_writer = logger.lock();
writeln!(locked_writer, "{}", format!($($arg)*))
};
}
}
};
}
#[cfg(not(feature = "debug_logging"))]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {
()
};
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
#[cfg(feature = "debug_logging")]
use crate::debug_log;
#[cfg(feature = "full_profiling")]
use crate::{safe_alloc, ProfileType};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
#[test]
fn test_logging_functionality() {
#[cfg(feature = "full_profiling")]
safe_alloc! {
let _ =
crate::profiling::test_utils::initialize_profiling_for_test(ProfileType::Memory);
};
#[cfg(all(feature = "time_profiling", not(feature = "full_profiling")))]
crate::profiling::force_enable_profiling_time_for_tests();
let debug_level = get_debug_level();
eprintln!("Current debug level: {:?}", debug_level);
let log_path = get_debug_log_path();
eprintln!("Debug log path: {:?}", log_path);
if matches!(debug_level, DebugLevel::None) {
eprintln!("Debug level is None, skipping remaining logging tests");
return;
}
let unique_msg = format!(
"Unique test message: {}",
chrono::Local::now().format("%H:%M:%S.%3f")
);
debug_log!("{}", unique_msg);
flush_debug_log();
let path = log_path.as_ref().unwrap();
assert!(Path::new(path).exists(), "Log file should exist");
let file = File::open(path).expect("Failed to open log file");
let reader = BufReader::new(file);
#[cfg(feature = "full_profiling")]
let found_message = safe_alloc! {
reader
.lines()
.map_while(Result::ok)
.any(|line| line.contains(&unique_msg))
};
#[cfg(not(feature = "full_profiling"))]
let found_message = reader
.lines()
.filter_map(Result::ok)
.any(|line| line.contains(&unique_msg));
assert!(found_message, "Log should contain our test message");
let logger = DebugLogger::get();
assert!(logger.is_some(), "Logger should be available");
if let Some(logger_mutex) = logger {
let write_result = {
let mut locked_logger = logger_mutex.lock();
writeln!(locked_logger, "Direct logger write test")
};
assert!(write_result.is_ok(), "Direct write should succeed");
}
for i in 0..5 {
debug_log!("Auto-flush test message #{}", i);
}
flush_debug_log();
#[cfg(feature = "full_profiling")]
{
let current_allocator = safe_alloc!(crate::mem_tracking::current_allocator());
debug_log!("Testing system allocator usage");
let after_allocator = safe_alloc!(crate::mem_tracking::current_allocator());
assert_eq!(
current_allocator, after_allocator,
"Allocator should remain unchanged after logging"
);
}
crate::profiling::disable_profiling();
eprintln!("All logging tests completed successfully!");
}
#[test]
fn test_create_debug_logger() {
let logger = create_debug_logger();
if matches!(get_debug_level(), DebugLevel::None) {
assert!(
logger.is_none(),
"Logger should be None with debug level None"
);
}
}
}