Skip to content

fix(logging): close rotated log fd immediately + expose logging.rotation.retention (#683) - #1687

Merged
kriszyp merged 3 commits into
mainfrom
kris/log-rotator-fd-683
Jul 10, 2026
Merged

fix(logging): close rotated log fd immediately + expose logging.rotation.retention (#683)#1687
kriszyp merged 3 commits into
mainfrom
kris/log-rotator-fd-683

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 7, 2026

Copy link
Copy Markdown
Member

Fixes #683 — two log-management issues in the rotator.

1. FD handling during rotation (root cause)

moveLogFile closed the module-global descriptor (hdbLogger.closeLogFile(), which only touches 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 file open — and with compress enabled, the subsequently unlinked inode — until the logger's 10s safety timeout fired. Effects per rotation:

  • Disk space pinned on the deleted inode for up to 10s.
  • Up to 10s of log writes (including the "hdb.log rotated" notify) landing in the moved/deleted file instead of the fresh hdb.log.

Under high write volume + small maxSize, this recurs continuously and presents as leaking fds / disk to lsof/df.

Fix: close the logger's own closeLogFile immediately 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 ENOENT handler in the size check returned 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.retention

The 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's allowUnknown, so it was undocumented and unvalidated. Exposed it in both, with a D/H/M duration validator mirroring the existing interval/maxSize validators, plus schema docs. (Reusing the already-wired retention rather than adding a new maxAgeDays — 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).
  • Full logging dir: 51 passing. tsc build clean.

Cross-model review (Gemini + Harper domain adjudication)

No blockers or regressions. Gemini's two "blocker" flags were adjudicated out:

  • "nullish-coalescing drops this, crashes on this.logFD"refuted: both closeLogFile variants are plain closures (over logFD / mainLogFd), not instance methods; no this.
  • "throw err in 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 readdir in try/catch so a not-yet-created rotated dir can't throw out of the tick.

Noted for later (pre-existing, not touched): the interval validator message says M (minutes) but convertToMS treats capital M as months; retention's docs/message follow the actual behavior (M=months).

…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>
@kriszyp
kriszyp removed the request for review from cb1kenobi July 7, 2026 12:27

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread validation/configValidator.ts
Comment thread unitTests/utility/logging/logRotator.test.js Outdated
Comment thread unitTests/validation/configValidator.test.js
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

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>
@kriszyp

kriszyp commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Follow-up commit (46494d2): corrected the rotation duration units rather than just noting the discrepancy.

convertToMS treats capital M as months and lowercase m as minutes. The existing interval validator both mislabeled M as "minutes" and rejected lowercase m entirely — so minute-granularity rotation intervals (e.g. 30m) were impossible despite convertToMS supporting them. Both the interval and the new retention validators now accept the full grammar convertToMS handles — D/d (days), H/h (hours), M (months), m (minutes) — with corrected messages and schema docs. Added a test that interval now accepts 30m.

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 Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix, and good catch on the units grammar.

sent with Claude Fable 5

@kriszyp
kriszyp merged commit 5eb223c into main Jul 10, 2026
49 checks passed
@kriszyp
kriszyp deleted the kris/log-rotator-fd-683 branch July 10, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Log rotator: potential FD leak + add option to delete old log files

2 participants