#[cfg(feature = "file-logging")]
use anyhow::Context;
use anyhow::Result;
#[cfg(feature = "file-logging")]
use chrono::Utc;
use std::path::{Path, PathBuf};
#[cfg(feature = "file-logging")]
use tracing_appender::rolling;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer, Registry};
use crate::cli::CrateDebugFlags;
pub struct LoggingGuard {
#[cfg(feature = "file-logging")]
_file_guards: Vec<tracing_appender::non_blocking::WorkerGuard>,
#[cfg(feature = "file-logging")]
log_dir: PathBuf,
}
impl LoggingGuard {
#[cfg(feature = "file-logging")]
pub fn log_dir(&self) -> &Path {
&self.log_dir
}
#[cfg(not(feature = "file-logging"))]
pub fn log_dir(&self) -> &Path {
Path::new(".")
}
}
#[cfg(feature = "file-logging")]
pub fn init_logging(
debug_flags: &CrateDebugFlags,
log_dir: Option<PathBuf>,
retention_days: Option<u64>,
retention_runs: Option<usize>,
) -> Result<LoggingGuard> {
let base_log_dir = log_dir.unwrap_or_else(|| PathBuf::from("./logs"));
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let run_folder = base_log_dir.join(format!("run_{}", timestamp));
std::fs::create_dir_all(&run_folder)
.with_context(|| format!("Failed to create log directory: {}", run_folder.display()))?;
cleanup_old_logs(&base_log_dir, retention_days, retention_runs)?;
let filter = debug_flags.to_filter_string();
let env_filter = EnvFilter::new(&filter);
let mut layers = Vec::new();
let mut file_guards = Vec::new();
let console_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_file(false)
.with_line_number(false)
.with_filter(env_filter.clone());
layers.push(console_layer.boxed());
for crate_name in crate::KNOWN_CRATES {
let file_appender = rolling::daily(&run_folder, format!("{}.log", crate_name));
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
file_guards.push(guard);
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_target(true)
.with_file(true)
.with_line_number(true)
.json()
.with_filter(EnvFilter::new(format!("{}=debug,info", crate_name)))
.boxed();
layers.push(file_layer);
}
let combined_appender = rolling::daily(&run_folder, "feagi.log");
let (combined_non_blocking, combined_guard) = tracing_appender::non_blocking(combined_appender);
let combined_layer = tracing_subscriber::fmt::layer()
.with_writer(combined_non_blocking)
.with_target(true)
.with_file(true)
.with_line_number(true)
.json()
.with_filter(env_filter.clone())
.boxed();
layers.push(combined_layer);
Registry::default().with(layers).init();
file_guards.push(combined_guard);
Ok(LoggingGuard {
_file_guards: file_guards,
log_dir: run_folder,
})
}
#[cfg(not(feature = "file-logging"))]
pub fn init_logging(
debug_flags: &CrateDebugFlags,
_log_dir: Option<PathBuf>,
_retention_days: Option<u64>,
_retention_runs: Option<usize>,
) -> Result<LoggingGuard> {
let filter = debug_flags.to_filter_string();
let env_filter = EnvFilter::new(&filter);
let console_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_file(false)
.with_line_number(false)
.with_filter(env_filter);
Registry::default().with(console_layer.boxed()).init();
Ok(LoggingGuard {})
}
#[cfg(feature = "file-logging")]
fn cleanup_old_logs(
base_log_dir: &Path,
retention_days: Option<u64>,
retention_runs: Option<usize>,
) -> Result<()> {
if !base_log_dir.exists() {
return Ok(());
}
let retention_days = retention_days.unwrap_or(30);
let retention_runs = retention_runs.unwrap_or(10);
let cutoff_date = Utc::now() - chrono::Duration::days(retention_days as i64);
let mut runs: Vec<(PathBuf, DateTime<Utc>)> = Vec::new();
for entry in std::fs::read_dir(base_log_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
if dir_name.starts_with("run_") {
if let Some(timestamp_str) = dir_name.strip_prefix("run_") {
if let Ok(dt) = DateTime::parse_from_str(timestamp_str, "%Y%m%d_%H%M%S") {
runs.push((path, dt.with_timezone(&Utc)));
}
}
}
}
}
}
runs.sort_by_key(|(_, dt)| *dt);
let mut removed_count = 0;
for (path, dt) in &runs {
if *dt < cutoff_date {
if let Err(e) = std::fs::remove_dir_all(path) {
eprintln!(
"Warning: Failed to remove old log directory {}: {}",
path.display(),
e
);
} else {
removed_count += 1;
}
}
}
if runs.len() - removed_count > retention_runs {
let to_remove = runs.len() - removed_count - retention_runs;
for (path, dt) in runs.iter().take(to_remove) {
if *dt >= cutoff_date {
if path.exists() {
if let Err(e) = std::fs::remove_dir_all(path) {
eprintln!(
"Warning: Failed to remove old log directory {}: {}",
path.display(),
e
);
}
}
}
}
}
Ok(())
}
pub fn init_logging_default(debug_flags: &CrateDebugFlags) -> Result<LoggingGuard> {
init_logging(debug_flags, None, None, None)
}