use crate::{domain::error::GitTypeError, Result};
use chrono;
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Config, Logger, Root},
encode::pattern::PatternEncoder,
};
use std::path::PathBuf;
use std::sync::OnceLock;
static CURRENT_LOG_FILE: OnceLock<String> = OnceLock::new();
pub fn setup_logging() -> Result<()> {
let log_dir = get_log_directory()?;
std::fs::create_dir_all(&log_dir)?;
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let log_file = log_dir.join(format!("gittype_{}.log", timestamp));
let _ = CURRENT_LOG_FILE.set(log_file.display().to_string());
let file_appender = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S)} [{l}] {t} - {m}\n",
)))
.build(log_file)
.map_err(|e| {
GitTypeError::database_error(format!("Failed to create file appender: {}", e))
})?;
let console_appender = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new("[{l}] {m}\n")))
.build();
let config = Config::builder()
.appender(Appender::builder().build("file", Box::new(file_appender)))
.appender(Appender::builder().build("console", Box::new(console_appender)))
.logger(Logger::builder().build("globset", log::LevelFilter::Off))
.logger(Logger::builder().build("ignore", log::LevelFilter::Off))
.build(Root::builder().appender("file").build(get_log_level()))
.map_err(|e| GitTypeError::database_error(format!("Failed to build log config: {}", e)))?;
log4rs::init_config(config).map_err(|e| {
GitTypeError::database_error(format!("Failed to initialize logging: {}", e))
})?;
log::info!(
"GitType logging initialized, logs saved to: {}",
log_dir.display()
);
Ok(())
}
pub fn get_log_directory() -> Result<PathBuf> {
if cfg!(test) {
use tempfile::TempDir;
let temp_dir = TempDir::new().map_err(|e| {
GitTypeError::ExtractionFailed(format!("Failed to create temp log directory: {}", e))
})?;
let path = temp_dir.path().join("logs");
std::fs::create_dir_all(&path).map_err(|e| {
GitTypeError::ExtractionFailed(format!("Failed to create test log directory: {}", e))
})?;
std::mem::forget(temp_dir);
Ok(path)
} else if cfg!(debug_assertions) {
let current_dir = std::env::current_dir().map_err(|e| {
GitTypeError::ExtractionFailed(format!("Could not get current directory: {}", e))
})?;
Ok(current_dir.join("logs"))
} else {
let home_dir = dirs::home_dir().ok_or_else(|| {
GitTypeError::ExtractionFailed("Could not determine home directory".to_string())
})?;
Ok(home_dir.join(".gittype").join("logs"))
}
}
pub fn get_current_log_file_path() -> Option<String> {
CURRENT_LOG_FILE.get().cloned()
}
fn get_log_level() -> log::LevelFilter {
if cfg!(debug_assertions) {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
}
}
pub fn setup_console_logging() {
let console_appender = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new("[{l}] {m}\n")))
.build();
if let Ok(config) = Config::builder()
.appender(Appender::builder().build("console", Box::new(console_appender)))
.build(
Root::builder()
.appender("console")
.build(log::LevelFilter::Warn), )
{
let _ = log4rs::init_config(config);
}
}
pub fn log_panic_to_file(panic_info: &std::panic::PanicHookInfo) {
let panic_message = format_panic_info(panic_info);
if try_log_panic(&panic_message).is_err() {
write_panic_to_file(&panic_message);
}
}
pub fn log_error_to_file(error: &GitTypeError) {
use std::error::Error;
let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let mut error_info = String::new();
error_info.push_str(&format!("ERROR OCCURRED AT: {}\n", timestamp));
error_info.push_str(&format!(
"ERROR TYPE: {:?}\n",
std::any::type_name_of_val(error)
));
error_info.push_str(&format!("ERROR MESSAGE: {}\n", error));
let mut current_error = error.source();
let mut level = 1;
while let Some(err) = current_error {
error_info.push_str(&format!("CAUSED BY (level {}): {}\n", level, err));
current_error = err.source();
level += 1;
}
error_info.push_str(&get_environment_context());
log::error!("APPLICATION ERROR:\n{}", error_info);
write_error_to_file(&error_info);
}
fn format_panic_info(panic_info: &std::panic::PanicHookInfo) -> String {
let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let mut info = String::new();
info.push_str(&format!("PANIC OCCURRED AT: {}\n", timestamp));
info.push_str(&format!("PANIC INFO: {}\n", panic_info));
if let Some(location) = panic_info.location() {
info.push_str(&format!(
"PANIC LOCATION: {}:{}:{}\n",
location.file(),
location.line(),
location.column()
));
}
if let Some(payload) = panic_info.payload().downcast_ref::<&str>() {
info.push_str(&format!("PANIC MESSAGE: {}\n", payload));
} else if let Some(payload) = panic_info.payload().downcast_ref::<String>() {
info.push_str(&format!("PANIC MESSAGE: {}\n", payload));
}
info.push_str(&get_environment_context());
info
}
pub fn get_environment_context() -> String {
use std::env;
let mut context = String::new();
if let Ok(exe) = env::current_exe() {
context.push_str(&format!("EXECUTABLE: {:?}\n", exe));
}
if let Ok(cwd) = env::current_dir() {
context.push_str(&format!("WORKING_DIR: {:?}\n", cwd));
}
let args: Vec<String> = env::args().collect();
context.push_str(&format!("COMMAND_ARGS: {:?}\n", args));
if let Ok(rust_backtrace) = env::var("RUST_BACKTRACE") {
context.push_str(&format!("RUST_BACKTRACE: {}\n", rust_backtrace));
}
if let Ok(rust_log) = env::var("RUST_LOG") {
context.push_str(&format!("RUST_LOG: {}\n", rust_log));
}
context.push_str(&format!("OS: {}\n", env::consts::OS));
context.push_str(&format!("ARCH: {}\n", env::consts::ARCH));
context
}
fn try_log_panic(message: &str) -> std::result::Result<(), ()> {
log::error!("PANIC DETAILS:\n{}", message);
Ok(())
}
fn write_panic_to_file(message: &str) {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let panic_log_path = format!("logs/panic_{}.log", timestamp);
if write_to_log_file(&panic_log_path, message).is_err() {
let fallback_path = format!("gittype_panic_{}.log", timestamp);
if let Ok(()) = write_to_log_file(&fallback_path, message) {
eprintln!("💾 Panic details written to: {}", fallback_path);
}
} else {
eprintln!("💾 Panic details written to: {}", panic_log_path);
}
}
fn write_error_to_file(error_info: &str) {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let error_log_path = format!("logs/error_{}.log", timestamp);
if write_to_log_file(&error_log_path, error_info).is_err() {
let fallback_path = format!("gittype_error_{}.log", timestamp);
if let Ok(()) = write_to_log_file(&fallback_path, error_info) {
eprintln!("💾 Error details written to: {}", fallback_path);
}
} else {
eprintln!("💾 Error details written to: {}", error_log_path);
}
}
fn write_to_log_file(path: &str, content: &str) -> std::io::Result<()> {
use std::fs::OpenOptions;
use std::io::Write;
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
writeln!(file, "{}", content)?;
Ok(())
}