|
27 | 27 | * 16384-char tail (UTF-8 byte size varies; |
28 | 28 | * idempotent backstop to migration v11) |
29 | 29 | * – prune batch_results to DEFAULT_BATCH_RETENTION |
| 30 | + * · truncate oversized audit-*.log files to their |
| 31 | + * last LOG_BUFFER_MAX (5000) lines — the only |
| 32 | + * tail the logBuffer/SSE init ever replays — after |
| 33 | + * backing the full file up to <name>.bak-<ts> |
30 | 34 | * --purge-orphan-logs (with --fix) quarantine orphan audit logs into |
31 | 35 | * results/.cleanup-trash/<ts>/ (move, not delete; |
32 | 36 | * never the active or a recently-written log). |
@@ -66,6 +70,11 @@ const BACKFILL_TOLERANCE_MS = 60_000; // wider dedup window for backfill (save-t |
66 | 70 | const RECENT_LOG_MS = 24 * 60 * 60 * 1000; // never purge a log written in the last day |
67 | 71 | const RUNAWAY_NOTES = 'continuous-loop iteration%'; |
68 | 72 | const LOG_SNIPPET_CAP = 16384; // 16384 chars (UTF-8 byte size varies) — mirrors core/db.js insertErrorLog + migration v11 |
| 73 | +const LOG_BUFFER_MAX = 5000; // lines — mirrors server.js logBuffer cap. On boot/resume the logBuffer |
| 74 | + // hydrates from an audit-*.log via lines.slice(-LOG_BUFFER_MAX) (server.js |
| 75 | + // ~692), and the SSE init frame replays at most INIT_LOG_REPLAY of those. |
| 76 | + // Anything beyond the last LOG_BUFFER_MAX lines is never read back, so an |
| 77 | + // audit log longer than this carries a permanently-unused head. |
69 | 78 |
|
70 | 79 | // Human-readable byte size for the slim-down report. |
71 | 80 | const humanBytes = n => { |
@@ -318,6 +327,36 @@ if (!existsSync(RAW_DIR)) { |
318 | 327 | else { repairable(`${orphanRawDirs.length} orphan raw dir(s), ${humanBytes(orphanRawBytes)} total reclaimable`); if (!FIX) console.log(C.dim(' (pass --fix to rm -rf these orphan dirs)')); } |
319 | 328 | } |
320 | 329 |
|
| 330 | +// ─── 8. Oversized audit logs (truncate head, keep last LOG_BUFFER_MAX lines) ── |
| 331 | +// The logBuffer (and thus the SSE init frame) only ever reads the TAIL of an |
| 332 | +// audit-*.log — resume hydrates via lines.slice(-LOG_BUFFER_MAX) (server.js |
| 333 | +// ~692). A log longer than LOG_BUFFER_MAX lines therefore has a head that is |
| 334 | +// never replayed; capping it to the last LOG_BUFFER_MAX lines loses nothing the |
| 335 | +// app consumes (the active/in-flight log included — resume slices identically). |
| 336 | +// The full pre-truncation file is backed up to <name>.bak-<ts> before the |
| 337 | +// rewrite, so historical context is preserved for manual review/deletion. |
| 338 | +section('8. Oversized audit logs'); |
| 339 | +let oversizedLogs = []; // { name, lines } |
| 340 | +if (existsSync(RESULTS_DIR)) { |
| 341 | + const logs = readdirSync(RESULTS_DIR).filter(f => /^(audit|retest)-.*\.log$/.test(f)); |
| 342 | + for (const f of logs) { |
| 343 | + let lines = 0; |
| 344 | + try { |
| 345 | + // Count newlines without holding a split array: cheap and bounded-memory |
| 346 | + // enough for the occasional server-stopped cleanup run. |
| 347 | + const buf = readFileSync(path.join(RESULTS_DIR, f)); |
| 348 | + for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++; |
| 349 | + if (buf.length && buf[buf.length - 1] !== 0x0a) lines++; // last line w/o trailing \n |
| 350 | + } catch (e) { hard(`could not read ${f} for line count: ${e.message}`); continue; } |
| 351 | + if (lines > LOG_BUFFER_MAX) oversizedLogs.push({ name: f, lines }); |
| 352 | + } |
| 353 | + if (!oversizedLogs.length) ok(`no oversized audit logs (${logs.length} log file(s), all ≤ ${LOG_BUFFER_MAX.toLocaleString()} lines)`); |
| 354 | + else { |
| 355 | + for (const o of oversizedLogs) repairable(`${o.name}: ${o.lines.toLocaleString()} lines > ${LOG_BUFFER_MAX.toLocaleString()} cap (would keep last ${LOG_BUFFER_MAX.toLocaleString()}; head is never replayed)`); |
| 356 | + if (!FIX) console.log(C.dim(` (pass --fix to truncate to the last ${LOG_BUFFER_MAX.toLocaleString()} lines — full file backed up first)`)); |
| 357 | + } |
| 358 | +} |
| 359 | + |
321 | 360 | // Close our OWN read-only handle before mutating: if it stays open, the WAL |
322 | 361 | // checkpoint below can report `busy` and falsely abort the whole --fix on a |
323 | 362 | // perfectly healthy, server-stopped box. |
@@ -509,6 +548,33 @@ if (FIX) { |
509 | 548 | } catch (e) { hard(`orphan raw-dir reclaim failed: ${e.message}`); } |
510 | 549 | } else if (!dbWriteOk) console.log(` ${C.dim('↳ orphan raw-dir reclaim skipped (DB writes disabled)')}`); |
511 | 550 |
|
| 551 | + // ── Truncate oversized audit logs to their last LOG_BUFFER_MAX lines ────── |
| 552 | + // Gated on !abortAll (a blocked WAL checkpoint means the server is running and |
| 553 | + // may be appending to the active log — never rewrite a log under it). Back the |
| 554 | + // full file up to <name>.bak-<ts> BEFORE the rewrite, so a crash mid-write or a |
| 555 | + // later "wanted that history" leaves the original recoverable. Logs already |
| 556 | + // being quarantined wholesale by --purge-orphan-logs are skipped (the move |
| 557 | + // takes the whole file; truncating it first would be wasted work). |
| 558 | + if (oversizedLogs.length && !abortAll) { |
| 559 | + const purgeSet = (PURGE_LOGS) ? new Set(orphanLogs) : new Set(); |
| 560 | + for (const o of oversizedLogs) { |
| 561 | + if (purgeSet.has(o.name)) { console.log(` ${C.dim(`↳ ${o.name}: skipped truncate (being quarantined by --purge-orphan-logs)`)}`); continue; } |
| 562 | + const src = path.join(RESULTS_DIR, o.name); |
| 563 | + try { |
| 564 | + const lines = readFileSync(src, 'utf8').split('\n'); |
| 565 | + // split('\n') on a trailing-newline file yields a final '' element; drop |
| 566 | + // it so the kept count is real lines, then re-add one trailing newline. |
| 567 | + if (lines.length && lines[lines.length - 1] === '') lines.pop(); |
| 568 | + const kept = lines.slice(-LOG_BUFFER_MAX); |
| 569 | + copyFileSync(src, `${src}.bak-${ts}`); |
| 570 | + writeFileSync(src, kept.join('\n') + '\n', 'utf8'); |
| 571 | + fixd(`truncated ${o.name} → last ${kept.length.toLocaleString()} lines (was ${o.lines.toLocaleString()}; full file → ${o.name}.bak-${ts})`); |
| 572 | + } catch (e) { hard(`could not truncate ${o.name}: ${e.message}`); } |
| 573 | + } |
| 574 | + } else if (oversizedLogs.length && abortAll) { |
| 575 | + console.log(` ${C.dim('↳ oversized-log truncate skipped (server appears to be running)')}`); |
| 576 | + } |
| 577 | + |
512 | 578 | if (PURGE_LOGS && orphanLogs.length && !abortAll) { |
513 | 579 | const trash = path.join(RESULTS_DIR, '.cleanup-trash', String(ts)); |
514 | 580 | try { |
|
0 commit comments