use std::path::PathBuf;
use tracing::Level;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
struct LocalTime;
impl FormatTime for LocalTime {
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
let now = chrono::Local::now();
write!(w, "{}", now.format("%Y-%m-%dT%H:%M:%S%.6f%:z"))
}
}
#[derive(Debug, Clone)]
pub struct LogConfig {
pub debug_mode: bool,
pub log_dir: PathBuf,
pub log_level: Level,
pub console_output: bool,
pub log_prefix: String,
pub max_age_days: u64,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
debug_mode: false,
log_dir: crate::config::opencrabs_home().join("logs"),
log_level: Level::INFO,
console_output: false,
log_prefix: "opencrabs".to_string(),
max_age_days: 7,
}
}
}
impl LogConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_debug_mode(mut self, enabled: bool) -> Self {
self.debug_mode = enabled;
if enabled {
self.log_level = Level::DEBUG;
}
self
}
pub fn with_log_dir(mut self, dir: PathBuf) -> Self {
self.log_dir = dir;
self
}
pub fn with_log_level(mut self, level: Level) -> Self {
self.log_level = level;
self
}
pub fn with_console_output(mut self, enabled: bool) -> Self {
self.console_output = enabled;
self
}
pub fn with_log_prefix(mut self, prefix: String) -> Self {
self.log_prefix = prefix;
self
}
}
pub struct LoggerGuard {
_guard: Option<WorkerGuard>,
}
impl LoggerGuard {
fn with_guard(guard: WorkerGuard) -> Self {
Self {
_guard: Some(guard),
}
}
fn empty() -> Self {
Self { _guard: None }
}
}
pub fn init_logging(config: LogConfig) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
if config.debug_mode {
init_debug_logging(config)
} else {
init_minimal_logging(config)
}
}
fn init_debug_logging(config: LogConfig) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
std::fs::create_dir_all(&config.log_dir)?;
let opencrabs_dir = config.log_dir.parent().unwrap_or(&config.log_dir);
let gitignore_path = opencrabs_dir.join(".gitignore");
if !gitignore_path.exists() {
std::fs::write(
&gitignore_path,
"# Ignore all OpenCrabs runtime files\n*\n!.gitignore\n",
)
.ok();
}
let file_appender = tracing_appender::rolling::daily(&config.log_dir, &config.log_prefix);
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter = EnvFilter::from_default_env()
.add_directive(config.log_level.into())
.add_directive("rusqlite=warn".parse()?)
.add_directive("hyper=warn".parse()?)
.add_directive("h2=warn".parse()?)
.add_directive("reqwest=warn".parse()?)
.add_directive("tower=warn".parse()?)
.add_directive("slack_morphism=warn".parse()?)
.add_directive("whatsapp_rust::client=error".parse()?)
.add_directive("whatsapp_rust=warn".parse()?);
tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_timer(LocalTime)
.with_ansi(false) .with_target(true)
.with_thread_ids(true)
.with_line_number(true)
.with_file(true),
)
.init();
tracing::info!("🚀 OpenCrabs debug mode enabled");
tracing::info!("📁 Log directory: {}", config.log_dir.display());
tracing::info!("📊 Log level: {:?}", config.log_level);
tracing::debug!("Debug logging initialized successfully");
Ok(LoggerGuard::with_guard(guard))
}
fn init_minimal_logging(config: LogConfig) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
let env_filter = EnvFilter::from_default_env()
.add_directive(Level::WARN.into()) .add_directive("opencrabs=info".parse()?);
if config.console_output {
tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_timer(LocalTime)
.with_ansi(true)
.with_target(false)
.compact(),
)
.init();
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::sink))
.init();
}
Ok(LoggerGuard::empty())
}
pub fn setup_from_cli(debug: bool) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
let config = LogConfig::new().with_debug_mode(debug);
init_logging(config)
}
pub fn get_log_path() -> Option<PathBuf> {
let log_dir = crate::config::opencrabs_home().join("logs");
if log_dir.exists() {
std::fs::read_dir(&log_dir)
.ok()?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.path()
.extension()
.map(|ext| ext == "log")
.unwrap_or(false)
})
.max_by_key(|entry| entry.metadata().ok()?.modified().ok())
.map(|entry| entry.path())
} else {
None
}
}
pub fn cleanup_old_logs(max_age_days: u64) -> Result<usize, Box<dyn std::error::Error>> {
let log_dir = crate::config::opencrabs_home().join("logs");
if !log_dir.exists() {
return Ok(0);
}
let max_age = std::time::Duration::from_secs(max_age_days * 24 * 60 * 60);
let now = std::time::SystemTime::now();
let mut removed = 0;
for entry in std::fs::read_dir(&log_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|ext| ext == "log").unwrap_or(false)
&& let Ok(metadata) = entry.metadata()
&& let Ok(modified) = metadata.modified()
&& let Ok(age) = now.duration_since(modified)
&& age > max_age
&& std::fs::remove_file(&path).is_ok()
{
removed += 1;
}
}
Ok(removed)
}
pub fn cleanup_old_temp_files(max_age_days: u64) -> Result<usize, Box<dyn std::error::Error>> {
let home = match dirs::home_dir() {
Some(h) => h,
None => return Ok(0),
};
let tmp_dir = home.join(".opencrabs").join("tmp").join("files");
if !tmp_dir.exists() {
return Ok(0);
}
let max_age = std::time::Duration::from_secs(max_age_days * 24 * 60 * 60);
let now = std::time::SystemTime::now();
let mut removed = 0;
for entry in std::fs::read_dir(&tmp_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if let Ok(metadata) = entry.metadata()
&& let Ok(modified) = metadata.modified()
&& let Ok(age) = now.duration_since(modified)
&& age > max_age
&& std::fs::remove_file(&path).is_ok()
{
removed += 1;
}
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_config_default() {
let config = LogConfig::default();
assert!(!config.debug_mode);
assert_eq!(config.log_level, Level::INFO);
assert!(!config.console_output);
assert_eq!(config.log_prefix, "opencrabs");
}
#[test]
fn test_log_config_with_debug() {
let config = LogConfig::new().with_debug_mode(true);
assert!(config.debug_mode);
assert_eq!(config.log_level, Level::DEBUG);
}
#[test]
fn test_log_config_builder() {
let config = LogConfig::new()
.with_log_level(Level::TRACE)
.with_console_output(true)
.with_log_prefix("test".to_string());
assert_eq!(config.log_level, Level::TRACE);
assert!(config.console_output);
assert_eq!(config.log_prefix, "test");
}
#[test]
fn test_log_dir_in_home_opencrabs_folder() {
let config = LogConfig::default();
let log_dir_str = config.log_dir.to_string_lossy();
assert!(log_dir_str.contains(".opencrabs"));
assert!(log_dir_str.contains("logs"));
if let Some(home) = dirs::home_dir() {
assert!(config.log_dir.starts_with(&home));
}
}
}