fix(logging): external/component loggers inherit main rotation config#1880
fix(logging): external/component loggers inherit main rotation config#1880kriszyp wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the updateLogger function in utility/logging/harper_logger.ts to allow external or component loggers to inherit the main logger's rotation configuration when they do not define their own. It also ensures that the main logger's rotation can still be cleared without getting stuck in a self-referential fallback. Additionally, comprehensive unit tests have been added to verify this inheritance behavior, explicit overrides, and the ability to clear the main logger's rotation. There are no review comments, and I have no feedback to provide.
|
Reviewed; no blockers found. |
…ollisions updateLogger() now lets external/component loggers inherit the main rotator's config (#1877), and those loggers commonly share the main log's directory — so their rotators also default to the same `<dir>/rotated` destination. moveLogFile() named every archive `HDB-<timestamp>.log` regardless of source, so two loggers rotating in the same audit tick could compute the identical filename and one POSIX rename would silently clobber the other's archive. Name each archive after its source log (hdb, external, a component name, ...) instead of the fixed "HDB" literal, and add a regression test that rotates two distinct logger paths into a shared directory at the same frozen instant and asserts both archives survive. Also convert the Chai assertions in the #1877 rotation-inheritance tests to node:assert/strict per repo convention — the surrounding Rewire access to private module state (updateLogger/mainLogger) is kept as-is, since there's no public seam to reach that behavior without it. Refs #1877, addresses code review on #1880. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-model review on #1880 flagged four issues in the original inheritance fix: - logRotator.ts: archive names collided when two sources shared a basename (e.g. `/logs/a/hdb.log` vs `/logs/b/hdb.log`) and rotated in the same millisecond, since only a timestamp disambiguated them. Names now also include a hash of the full resolved source path plus a pid+monotonic sequence suffix, so rename() can never clobber another source's archive. - logRotator.ts: a non-ENOENT error inside the async audit-interval callback was thrown but never awaited by setInterval, leaking an unhandled rejection and skipping retention cleanup for that tick. The whole tick body is now wrapped in try/catch and reported via the logger. - harper_logger.ts: a component logger inheriting main's rotation wholesale also inherited `rotation.path`, which fails with EXDEV if the component log lives on a different filesystem. The inherited path is now stripped (an explicit rotation.path in the component's own config still wins), defaulting rotation beside the component's own log. - Tests: the new coverage now goes through `updateLogger` with an explicit `mainLoggerRef` parameter (defaulting to the module's real singleton) instead of `rewire`/`__get__`/`__set__` private-state access, and the logRotator regression test waits for the actual two-archive condition instead of a fixed sleep, with equal basenames and distinct payload markers proving both logs survive. Refs #1877 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| Date.now = () => frozenNow; | ||
| const sharedRotatedDir = path.join(collisionDir, 'rotated'); | ||
| let sourceARotator, sourceBRotator; | ||
| try { | ||
| sourceARotator = log_rotator({ | ||
| logger: sourceALogger, | ||
| path: sharedRotatedDir, | ||
| enabled: true, | ||
| auditInterval: 100, | ||
| maxSize: '1K', | ||
| }); | ||
| sourceBRotator = log_rotator({ | ||
| logger: sourceBLogger, | ||
| path: sharedRotatedDir, | ||
| enabled: true, | ||
| auditInterval: 100, | ||
| maxSize: '1K', | ||
| }); | ||
| // Wait for the actual completion condition (both archives present) instead of a fixed | ||
| // sleep: end() only cancels *future* ticks, it does not await a stat()/rename() already | ||
| // in flight, so a fixed sleep can race that in-flight work and observe only one archive | ||
| // on a loaded runner. | ||
| await waitFor(() => fs.existsSync(sharedRotatedDir) && fs.readdirSync(sharedRotatedDir).length >= 2, { |
There was a problem hiding this comment.
Suggestion (non-blocking): Date.now is frozen at line 180 before waitFor is called, but waitFor's own deadline (unitTests/waitFor.js) is computed as Date.now() + timeout — with the clock frozen, that deadline never advances, so the Date.now() >= deadline timeout check can never trip. If this test's collision fix ever regresses (only one archive lands in sharedRotatedDir), this waitFor won't produce its intended friendly assert.fail(message) — it'll spin until Mocha's own .timeout(TEST_TIMEOUT) kills it with a generic timeout error instead. Consider restoring the real Date.now (or computing the deadline from the real clock before freezing) around this waitFor call so the bounded-wait/friendly-message behavior still works on a regression.
…gers (#1877) updateLogger() unconditionally set logger.rotation = logOptions.rotation, so an external or component logger configured with its own path but no rotation block lost rotation entirely instead of inheriting the main logging.rotation config (including maxSize), even though the docs say components default to main logging unless overridden. Fall back to mainLogger.rotation when the logger's own options don't specify rotation, mirroring the existing level/path fallback pattern in the same function — excluding the main logger itself so its own rotation can still be cleared by omitting the config block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ollisions updateLogger() now lets external/component loggers inherit the main rotator's config (#1877), and those loggers commonly share the main log's directory — so their rotators also default to the same `<dir>/rotated` destination. moveLogFile() named every archive `HDB-<timestamp>.log` regardless of source, so two loggers rotating in the same audit tick could compute the identical filename and one POSIX rename would silently clobber the other's archive. Name each archive after its source log (hdb, external, a component name, ...) instead of the fixed "HDB" literal, and add a regression test that rotates two distinct logger paths into a shared directory at the same frozen instant and asserts both archives survive. Also convert the Chai assertions in the #1877 rotation-inheritance tests to node:assert/strict per repo convention — the surrounding Rewire access to private module state (updateLogger/mainLogger) is kept as-is, since there's no public seam to reach that behavior without it. Refs #1877, addresses code review on #1880. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-model review on #1880 flagged four issues in the original inheritance fix: - logRotator.ts: archive names collided when two sources shared a basename (e.g. `/logs/a/hdb.log` vs `/logs/b/hdb.log`) and rotated in the same millisecond, since only a timestamp disambiguated them. Names now also include a hash of the full resolved source path plus a pid+monotonic sequence suffix, so rename() can never clobber another source's archive. - logRotator.ts: a non-ENOENT error inside the async audit-interval callback was thrown but never awaited by setInterval, leaking an unhandled rejection and skipping retention cleanup for that tick. The whole tick body is now wrapped in try/catch and reported via the logger. - harper_logger.ts: a component logger inheriting main's rotation wholesale also inherited `rotation.path`, which fails with EXDEV if the component log lives on a different filesystem. The inherited path is now stripped (an explicit rotation.path in the component's own config still wins), defaulting rotation beside the component's own log. - Tests: the new coverage now goes through `updateLogger` with an explicit `mainLoggerRef` parameter (defaulting to the module's real singleton) instead of `rewire`/`__get__`/`__set__` private-state access, and the logRotator regression test waits for the actual two-archive condition instead of a fixed sleep, with equal basenames and distinct payload markers proving both logs survive. Refs #1877 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
0937fdc to
cc374d0
Compare
Summary
updateLogger()unconditionally setlogger.rotation = logOptions.rotation, so an external or component logger configured with its ownpathbut norotationblock of its own lost rotation entirely — instead of inheritinglogging.rotation(includingmaxSize) from the main logger, as documented ("all components default to the main logging configuration unless overridden"). This lethdb.loggrow unbounded even withlogging.rotation.maxSizeset (reported as a 200G log file).Fix: fall back to
mainLogger.rotationwhen a logger's own options don't specify rotation, mirroring the existinglevel/pathfallback pattern already in the same function — excluding the main logger itself so its own rotation can still be cleared by omitting the rotation block from config.Refs #1877
Where to look
utility/logging/harper_logger.tsupdateLogger()— the one-line root cause + fix.unitTests/utility/logging/harper_logger.test.js— newdescribe('Test external/component logger rotation inheritance (#1877)')block: proves inheritance (incl.maxSize) with an actual file rotation (condition-waited, not a fixed sleep), that an explicit component-level{ enabled: false }override is preserved, and that the main logger's own rotation can still be cleared.Cross-model review
Ran the Gemini leg (standard mode); the
reviewer/codex-reviewersubagents aren't installed in this headless worker, so findings were adjudicated inline (noted as a caveat, per the skill's fallback protocol). Two real findings were fixed as part of this PR:mainLogger?.rotationread back its own current value whenlogger === mainLogger, making it impossible to ever clear the main logger's own rotation via config — excluded that case explicitly..pathmid-test, which left the public logger's.closeLogFilebound to a stale file entry;afterEachnow closes every registered file logger via the module's internal registry before removing the test directory.One suggestion (shallow-cloning the inherited rotation object instead of sharing the reference) was assessed and not applied — no code path in this file currently mutates a rotation config object in place after assignment, so cloning would be unused defensive complexity.
Test status
npm run test:unit:logging,test:unit:main,test:unit:resourcesall pass.npm run buildhas one pre-existing, unrelatedtscerror inresources/DatabaseTransaction.tspresent onorigin/mainbefore this change (confirmed viagit stash) —diststill emits despite it (noEmitOnErroris not set), so it doesn't block this PR, but it's worth a separate fix since this repo's build is supposed to be clean.Docs
No documentation change — the fix makes the code conform to the existing documented behavior (component loggers default to main logging config unless overridden); the docs were already correct.
Generated by Claude Sonnet 5 (dev-agent, dispatch harper-1877).
🤖 Generated with Claude Code