use std::fs;
use std::path::Path;
use tracing::{info, warn, error};
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter,
Layer,
};
use tracing_appender::{non_blocking, rolling};
use anyhow::Result;
#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub level: String,
pub console_enabled: bool,
pub file_enabled: bool,
pub log_dir: String,
pub file_prefix: String,
pub json_format: bool,
pub show_target: bool,
pub show_thread_ids: bool,
pub ansi_colors: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
console_enabled: true,
file_enabled: true,
log_dir: get_default_log_dir(),
file_prefix: "aicommit".to_string(),
json_format: false,
show_target: false,
show_thread_ids: false,
ansi_colors: true,
}
}
}
impl LoggingConfig {
pub fn new() -> Self {
let mut config = Self::default();
if let Ok(level) = std::env::var("AICOMMIT_LOG_LEVEL") {
config.level = level;
}
if let Ok(log_dir) = std::env::var("AICOMMIT_LOG_DIR") {
config.log_dir = log_dir;
}
if let Ok(_) = std::env::var("AICOMMIT_LOG_JSON") {
config.json_format = true;
}
if let Ok(_) = std::env::var("AICOMMIT_LOG_NO_COLOR") {
config.ansi_colors = false;
}
if let Ok(_) = std::env::var("AICOMMIT_LOG_VERBOSE") {
config.show_target = true;
config.show_thread_ids = true;
}
config
}
pub fn with_debug(&mut self) -> &mut Self {
self.level = "debug".to_string();
self.show_target = true;
self
}
#[allow(dead_code)]
pub fn with_trace(&mut self) -> &mut Self {
self.level = "trace".to_string();
self.show_target = true;
self.show_thread_ids = true;
self
}
#[allow(dead_code)]
pub fn file_only(&mut self) -> &mut Self {
self.console_enabled = false;
self
}
#[allow(dead_code)]
pub fn console_only(&mut self) -> &mut Self {
self.file_enabled = false;
self
}
}
fn get_default_log_dir() -> String {
if let Some(data_dir) = dirs::data_dir() {
data_dir.join("aicommit").join("logs").to_string_lossy().to_string()
} else {
"./logs".to_string()
}
}
pub fn init_logging(config: &LoggingConfig) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
if config.file_enabled {
ensure_log_dir(&config.log_dir)?;
}
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&config.level));
let mut layers = Vec::new();
let mut guard = None;
if config.console_enabled {
let console_layer = fmt::layer()
.with_ansi(config.ansi_colors)
.with_target(config.show_target)
.with_thread_ids(config.show_thread_ids)
.with_span_events(if config.level == "trace" {
FmtSpan::FULL
} else {
FmtSpan::NONE
})
.with_filter(env_filter.clone());
layers.push(console_layer.boxed());
}
if config.file_enabled {
let file_appender = rolling::daily(&config.log_dir, &config.file_prefix);
let (non_blocking_appender, worker_guard) = non_blocking(file_appender);
let file_layer = if config.json_format {
fmt::layer()
.json()
.with_writer(non_blocking_appender)
.with_target(true)
.with_thread_ids(config.show_thread_ids)
.with_span_events(FmtSpan::CLOSE)
.with_filter(env_filter.clone())
.boxed()
} else {
fmt::layer()
.with_writer(non_blocking_appender)
.with_ansi(false) .with_target(config.show_target)
.with_thread_ids(config.show_thread_ids)
.with_span_events(if config.level == "trace" {
FmtSpan::FULL
} else {
FmtSpan::CLOSE
})
.with_filter(env_filter)
.boxed()
};
layers.push(file_layer);
guard = Some(worker_guard);
}
tracing_subscriber::registry()
.with(layers)
.init();
info!("Logging system initialized");
info!("Log level: {}", config.level);
if config.file_enabled {
info!("Log directory: {}", config.log_dir);
}
Ok(guard)
}
fn ensure_log_dir(log_dir: &str) -> Result<()> {
let path = Path::new(log_dir);
if !path.exists() {
fs::create_dir_all(path)?;
info!("Created log directory: {}", log_dir);
}
Ok(())
}
#[macro_export]
macro_rules! operation_span {
($name:expr) => {
tracing::info_span!("operation", name = $name, id = %uuid::Uuid::new_v4())
};
($name:expr, $($key:ident = $value:expr),*) => {
tracing::info_span!("operation", name = $name, id = %uuid::Uuid::new_v4(), $($key = $value),*)
};
}
#[allow(dead_code)]
pub fn log_error<E>(error: E, context: &str) -> E
where
E: std::fmt::Display + std::fmt::Debug,
{
error!("{}: {}", context, error);
error
}
#[allow(dead_code)]
pub fn log_warning(message: &str, context: &str) {
warn!("{}: {}", context, message);
}
#[allow(dead_code)]
pub fn log_info(message: &str, context: &str) {
info!("{}: {}", context, message);
}
#[allow(dead_code)]
pub fn init_default_logging() -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
let config = LoggingConfig::new();
init_logging(&config)
}
#[allow(dead_code)]
pub fn init_dev_logging() -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
let mut config = LoggingConfig::new();
config.with_debug();
init_logging(&config)
}
#[allow(dead_code)]
pub fn init_prod_logging() -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
let mut config = LoggingConfig::new();
config.json_format = true;
config.ansi_colors = false;
init_logging(&config)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_logging_config_creation() {
let config = LoggingConfig::new();
assert_eq!(config.level, "info");
assert!(config.console_enabled);
assert!(config.file_enabled);
}
#[test]
fn test_logging_config_debug() {
let mut config = LoggingConfig::new();
config.with_debug();
assert_eq!(config.level, "debug");
assert!(config.show_target);
}
#[test]
fn test_log_dir_creation() {
let temp_dir = TempDir::new().unwrap();
let log_dir = temp_dir.path().join("test_logs").to_string_lossy().to_string();
ensure_log_dir(&log_dir).unwrap();
assert!(Path::new(&log_dir).exists());
}
}