diff --git a/docs-internal/logging-strategy.md b/docs-internal/logging-strategy.md index bca730944..f2c95a050 100644 --- a/docs-internal/logging-strategy.md +++ b/docs-internal/logging-strategy.md @@ -1,6 +1,6 @@ # Fabro Logging Strategy -Fabro uses the `tracing` crate for structured, file-based logging. Logs write to `~/.fabro/logs/YYYY-MM-DD.log`, controlled by the `FABRO_LOG` env var (default: `info`). Logs are for **developers debugging issues after the fact** — they are not user-facing output. +Fabro uses the `tracing` crate for structured, file-based logging. Logs write to `~/.fabro/logs/{prefix}.YYYY-MM-DD.log` (e.g. `cli.2026-04-06.log`, `server.2026-04-06.log`), rotated daily by `tracing-appender`. Logs older than 7 days are cleaned up on startup. Controlled by the `FABRO_LOG` env var (default: `info`). Logs are for **developers debugging issues after the fact** — they are not user-facing output. Production runs at INFO level. INFO should be low-volume and high-signal — the summary of what happened. When something goes wrong, developers enable `FABRO_LOG=debug` to get the full picture. DEBUG can be as verbose as needed since it's only turned on temporarily. diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index 59b8ae2b1..082bba25b 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -169,14 +169,11 @@ fn execute_daemon( return Ok(()); } - // Rotate logs let server_state = Storage::new(storage_dir).server_state(); let log_path = server_state.log_path(); if let Some(parent) = log_path.parent() { std::fs::create_dir_all(parent)?; } - let prev_path = log_path.with_extension("log.prev"); - let _ = std::fs::rename(&log_path, &prev_path); let record_path = server_state.record_path(); let log_file = std::fs::File::create(&log_path)?; diff --git a/lib/crates/fabro-cli/src/logging.rs b/lib/crates/fabro-cli/src/logging.rs index 59b7f9ca6..8a6905d9e 100644 --- a/lib/crates/fabro-cli/src/logging.rs +++ b/lib/crates/fabro-cli/src/logging.rs @@ -1,8 +1,12 @@ +use std::path::Path; + use anyhow::{Context, Result}; use fabro_util::run_log; use tracing_appender::rolling; use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; +const LOG_RETENTION_DAYS: u32 = 7; + pub(crate) fn init_tracing( debug: bool, config_log_level: Option<&str>, @@ -21,8 +25,14 @@ pub(crate) fn init_tracing( std::fs::create_dir_all(&log_dir) .with_context(|| format!("Failed to create log directory: {}", log_dir.display()))?; - let filename = format!("{log_prefix}.log"); - let file_appender = rolling::never(&log_dir, &filename); + let file_appender = rolling::RollingFileAppender::builder() + .rotation(rolling::Rotation::DAILY) + .filename_prefix(log_prefix) + .filename_suffix("log") + .build(&log_dir) + .with_context(|| "Failed to create log file appender")?; + + cleanup_old_logs(&log_dir, log_prefix, LOG_RETENTION_DAYS); let run_log_writer = run_log::init(); @@ -44,3 +54,35 @@ pub(crate) fn init_tracing( Ok(()) } + +fn cleanup_old_logs(log_dir: &Path, prefix: &str, max_age_days: u32) { + let cutoff = chrono::Utc::now().date_naive() - chrono::Duration::days(i64::from(max_age_days)); + let Ok(entries) = std::fs::read_dir(log_dir) else { + return; + }; + + let date_prefix = format!("{prefix}."); + let date_suffix = ".log"; + + for entry in entries.flatten() { + let name = entry.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + + let Some(rest) = name.strip_prefix(&date_prefix) else { + continue; + }; + let Some(date_str) = rest.strip_suffix(date_suffix) else { + continue; + }; + + let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") else { + continue; + }; + + if date < cutoff { + let _ = std::fs::remove_file(entry.path()); + } + } +}