perf(storage): prune error_log on startup with 90-day retention
The error_log table had no retention policy: every backend error was
appended forever, so across months of dogfooding it grew unbounded. That
silently bloats the diagnostic-bundle export and slows the
list_recent_errors query the Settings → About panel runs.
- New `kon_storage::prune_error_log(pool, keep_days)` does a single
`DELETE FROM error_log WHERE timestamp < datetime('now', '-Nd days')`
and returns the row count removed.
- src-tauri/src/lib.rs runs it once during setup() with a const
ERROR_LOG_RETENTION_DAYS = 90. Failure is logged to stderr but does not
block startup — a prune that fails is strictly less important than the
app coming up.
- Test: insert three rows at now / -30d / -200d, verify a 90-day prune
removes only the oldest, and a subsequent 14-day prune removes the
-30d row. Storage suite at 60/60.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
@@ -1155,6 +1155,26 @@ pub struct ErrorLogRow {
|
||||
pub metadata: Option<String>,
|
||||
}
|
||||
|
||||
/// Delete `error_log` rows older than `keep_days` whole days. Returns the
|
||||
/// row count removed.
|
||||
///
|
||||
/// Called once on app startup so the table doesn't grow unbounded across
|
||||
/// months of dogfooding — without this it would eventually balloon the
|
||||
/// diagnostic-bundle export and slow the `list_recent_errors` query.
|
||||
/// 90 days is the default: long enough to triage a "this happened a few
|
||||
/// weeks ago" report, short enough that the table stays kilobyte-scale.
|
||||
pub async fn prune_error_log(pool: &SqlitePool, keep_days: i64) -> Result<u64> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM error_log \
|
||||
WHERE timestamp < datetime('now', ?)",
|
||||
)
|
||||
.bind(format!("-{keep_days} days"))
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Prune error_log failed: {e}")))?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn list_recent_errors(pool: &SqlitePool, limit: i64) -> Result<Vec<ErrorLogRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, timestamp, context, error_code, message, metadata \
|
||||
@@ -2471,4 +2491,44 @@ mod tests {
|
||||
"must fail when DB file does not exist (no create_if_missing)",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prune_error_log_removes_old_rows_only() {
|
||||
let pool = test_pool().await;
|
||||
|
||||
// Three rows: one from now, one 30 days old, one 200 days old.
|
||||
sqlx::query(
|
||||
"INSERT INTO error_log (timestamp, context, error_code, message) VALUES \
|
||||
(datetime('now'), 'recent', NULL, 'now'), \
|
||||
(datetime('now', '-30 days'), 'older', NULL, '30d'), \
|
||||
(datetime('now', '-200 days'), 'oldest', NULL, '200d')",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Default 90-day retention removes only the 200-day row.
|
||||
let removed = prune_error_log(&pool, 90).await.unwrap();
|
||||
assert_eq!(removed, 1, "only the 200-day row should be pruned");
|
||||
|
||||
let remaining: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT context FROM error_log ORDER BY id ASC",
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(remaining, vec!["recent".to_string(), "older".to_string()]);
|
||||
|
||||
// A 14-day window prunes the 30-day row too.
|
||||
let removed = prune_error_log(&pool, 14).await.unwrap();
|
||||
assert_eq!(removed, 1);
|
||||
|
||||
let remaining: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT context FROM error_log",
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(remaining, vec!["recent".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user