feat(app): log retention — keep_recent_runs + retention_days (Enhancement 4)
LoggingCfg gains two fields with serde defaults: keep_recent_runs (default 100, top-N file retention) and retention_days (default 30, time-based retention for both ndjson files and the SQLite mirror). IngestLogWriter::open now runs cleanup_old_logs before creating a new ingest-*.ndjson — delete iff (idx >= keep_recent) OR (modified <= cutoff). ingest_with_config_opts also calls SqliteStore::prune_pdf_ocr_events(retention_days) at ingest start so the SQLite mirror tracks the same retention window. Backward compat (AC-9): both new fields use #[serde(default = ...)], so a pre-v0.20.x config with only [logging] ingest_log_enabled + ingest_log_dir parses unchanged. kebab init writes the new defaults automatically via Config::default() -> toml::to_string_pretty (AC-12). docs/SMOKE.md config example synced. Closure r1 F5: explicit OR-on-stale comment inside cleanup_old_logs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -443,9 +443,19 @@ pub struct LoggingCfg {
|
||||
|
||||
/// Directory for per-run log files. Default `{state_dir}/logs`.
|
||||
/// `{state_dir}` expands to the XDG state dir (e.g. `~/.local/state/kebab`).
|
||||
/// Log file accumulation is user-managed — no rotation policy (spec §6 R-1).
|
||||
#[serde(default = "default_ingest_log_dir")]
|
||||
pub ingest_log_dir: PathBuf,
|
||||
|
||||
/// v0.20.x r2 Enhancement 4: keep the most recent N ingest log files.
|
||||
/// Older files (beyond this count) are deleted at ingest start.
|
||||
/// Default 100. AC-9: #[serde(default)] ensures backward compat.
|
||||
#[serde(default = "default_keep_recent_runs")]
|
||||
pub keep_recent_runs: u32,
|
||||
|
||||
/// v0.20.x r2 Enhancement 4: delete log files older than N days.
|
||||
/// Also applied to `pdf_ocr_events` SQLite rows. Default 30.
|
||||
#[serde(default = "default_retention_days")]
|
||||
pub retention_days: u32,
|
||||
}
|
||||
|
||||
fn default_ingest_log_enabled() -> bool {
|
||||
@@ -454,12 +464,20 @@ fn default_ingest_log_enabled() -> bool {
|
||||
fn default_ingest_log_dir() -> PathBuf {
|
||||
PathBuf::from("{state_dir}/logs")
|
||||
}
|
||||
fn default_keep_recent_runs() -> u32 {
|
||||
100
|
||||
}
|
||||
fn default_retention_days() -> u32 {
|
||||
30
|
||||
}
|
||||
|
||||
impl Default for LoggingCfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ingest_log_enabled: default_ingest_log_enabled(),
|
||||
ingest_log_dir: default_ingest_log_dir(),
|
||||
keep_recent_runs: default_keep_recent_runs(),
|
||||
retention_days: default_retention_days(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,28 @@ fn pre_v020_config_without_logging_section_gets_defaults() {
|
||||
assert!(w.logging.ingest_log_enabled);
|
||||
assert_eq!(w.logging.ingest_log_dir, PathBuf::from("{state_dir}/logs"));
|
||||
}
|
||||
|
||||
// Test 4 (AC-9 v0.20.x r2): old config with only ingest_log_enabled + ingest_log_dir
|
||||
// parses without error and produces correct defaults for keep_recent_runs + retention_days.
|
||||
#[test]
|
||||
fn old_logging_config_parses_with_defaults() {
|
||||
let toml = r#"
|
||||
[logging]
|
||||
ingest_log_enabled = true
|
||||
ingest_log_dir = "{state_dir}/logs"
|
||||
"#;
|
||||
let w: LoggingWrapper = toml::from_str(toml).expect("old logging config must parse");
|
||||
assert!(w.logging.ingest_log_enabled);
|
||||
assert_eq!(
|
||||
w.logging.ingest_log_dir,
|
||||
PathBuf::from("{state_dir}/logs")
|
||||
);
|
||||
assert_eq!(
|
||||
w.logging.keep_recent_runs, 100,
|
||||
"keep_recent_runs must default to 100"
|
||||
);
|
||||
assert_eq!(
|
||||
w.logging.retention_days, 30,
|
||||
"retention_days must default to 30"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user