fix(logging): close rotated log fd immediately + expose logging.rotation.retention (#683) - #1687
Conversation
…tention (#683) Two log-management fixes for issue #683. FD handling during rotation: - `moveLogFile` closed the module-global descriptor (`hdbLogger.closeLogFile`, which targets `mainLogFd`) instead of the rotating logger's own descriptor. That never reset the logger's internal `logFD`, so after a rename the stale fd kept the moved (and, when compressing, subsequently unlinked) inode open until the logger's 10s safety timeout — pinning disk space and sending any writes in that window into the rotated/deleted file. Now the logger's own `closeLogFile` runs right after the rename so the next write reopens a fresh log file immediately. - The `ENOENT` early-return in the size-check aborted the entire audit tick, skipping retention cleanup whenever the active log was momentarily absent (idle logging or mid-rotation). Now a missing active log only skips the rotation checks; retention still runs. Old-log deletion (`logging.rotation.retention`): - The rotator already deleted rotated logs older than `retention`, but the key was never in the JSON schema or the Joi validator, so it was undocumented and unvalidated (only survived via Joi `allowUnknown`). Added it to both, with a duration validator (D/H/M) and schema docs. Tests: rotator now covers fd-reopen-after-rotation and retention deletion; validator covers retention unit/value/valid cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a log retention configuration option to automatically delete rotated logs older than a specified age, alongside validation logic and unit tests. The review feedback suggests hardening the validateRotationRetention validator to reject negative, zero, non-string, or empty values to prevent accidental log deletion or runtime TypeErrors. Additionally, the reviewer recommends updating the unit tests to use valid retention units (avoiding '30s') and expanding test coverage to verify these edge cases.
|
Reviewed; no blockers found. |
The rotation interval validator claimed "M (minutes)" but convertToMS treats capital M as months (86400*30s) and lowercase m as minutes — and the validator rejected lowercase m outright, so minute-granularity intervals were impossible despite being supported downstream. Align both the interval and the new retention validators with convertToMS's actual grammar: D/d (days), H/h (hours), M (months), m (minutes). Correct the unit messages and schema docs accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Follow-up commit (46494d2): corrected the rotation duration units rather than just noting the discrepancy.
|
Reject non-string/empty and non-positive retention values in validateRotationRetention. parseInt allowed '-30D'/'0D' through, which convertToMS turns into a <=0 retention window that deletes every rotated log immediately. Use parseFloat + strictly-positive check and guard the input is a non-empty string. Extend the validator unit test to cover the rejected cases and switch the rotator retention test to a valid unit (1H). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
Nice fix, and good catch on the units grammar.
sent with Claude Fable 5
Fixes #683 — two log-management issues in the rotator.
1. FD handling during rotation (root cause)
moveLogFileclosed the module-global descriptor (hdbLogger.closeLogFile(), which only touchesmainLogFd) instead of the rotating logger's own descriptor. That never reset the logger's internallogFD, so after a rename the stale fd kept the moved file open — and withcompressenabled, the subsequently unlinked inode — until the logger's 10s safety timeout fired. Effects per rotation:hdb.log.Under high write volume + small
maxSize, this recurs continuously and presents as leaking fds / disk tolsof/df.Fix: close the logger's own
closeLogFileimmediately after the rename (before the compress step, so the inode is released before the potentially-slow gzip). The next write then reopens a fresh log file right away.Also fixed an over-broad early-return: the
ENOENThandler in the size checkreturned from the whole audit tick, so retention cleanup was skipped whenever the active log was momentarily absent (idle logging or mid-rotation). Now a missing active log only skips the rotation checks; retention still runs.2. Old-log deletion —
logging.rotation.retentionThe rotator already deletes rotated logs older than
retention, but the key was absent from both the JSON schema and the Joi validator — it only worked accidentally via Joi'sallowUnknown, so it was undocumented and unvalidated. Exposed it in both, with aD/H/Mduration validator mirroring the existinginterval/maxSizevalidators, plus schema docs. (Reusing the already-wiredretentionrather than adding a newmaxAgeDays— the "or equivalent" the issue allowed.)Tests
logRotator.test.js: added reopen-a-fresh-file-after-rotation and retention-deletion cases (7 passing).configValidator.test.js: added retention unit/value/valid cases (49 passing).tscbuild clean.Cross-model review (Gemini + Harper domain adjudication)
No blockers or regressions. Gemini's two "blocker" flags were adjudicated out:
this, crashes onthis.logFD" — refuted: bothcloseLogFilevariants are plain closures (overlogFD/mainLogFd), not instance methods; nothis.throw errin the async tick crashes the process" — pre-existing, unchanged reachability (branch was only inverted). Legitimate hardening follow-up, out of scope here.Acted on the domain pass's one in-scope note: since the ENOENT change makes retention run in more ticks, wrapped the retention
readdirin try/catch so a not-yet-created rotated dir can't throw out of the tick.Noted for later (pre-existing, not touched): the
intervalvalidator message saysM (minutes)butconvertToMStreats capitalMas months; retention's docs/message follow the actual behavior (M=months).