Skip to content

fix(logging): external/component loggers inherit main rotation config#1880

Open
kriszyp wants to merge 3 commits into
mainfrom
fix/external-logger-rotation-inheritance
Open

fix(logging): external/component loggers inherit main rotation config#1880
kriszyp wants to merge 3 commits into
mainfrom
fix/external-logger-rotation-inheritance

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

updateLogger() unconditionally set logger.rotation = logOptions.rotation, so an external or component logger configured with its own path but no rotation block of its own lost rotation entirely — instead of inheriting logging.rotation (including maxSize) from the main logger, as documented ("all components default to the main logging configuration unless overridden"). This let hdb.log grow unbounded even with logging.rotation.maxSize set (reported as a 200G log file).

Fix: fall back to mainLogger.rotation when a logger's own options don't specify rotation, mirroring the existing level/path fallback 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.ts updateLogger() — the one-line root cause + fix.
  • unitTests/utility/logging/harper_logger.test.js — new describe('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-reviewer subagents 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:

  • A self-referential edge case where mainLogger?.rotation read back its own current value when logger === mainLogger, making it impossible to ever clear the main logger's own rotation via config — excluded that case explicitly.
  • New tests reassign a logger's .path mid-test, which left the public logger's .closeLogFile bound to a stale file entry; afterEach now 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:resources all pass. npm run build has one pre-existing, unrelated tsc error in resources/DatabaseTransaction.ts present on origin/main before this change (confirmed via git stash) — dist still emits despite it (noEmitOnError is 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

@kriszyp
kriszyp requested a review from Ethan-Arrowood July 20, 2026 20:15

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

@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp
kriszyp marked this pull request as ready for review July 21, 2026 02:30
@kriszyp
kriszyp marked this pull request as draft July 21, 2026 02:32
kriszyp added a commit that referenced this pull request Jul 21, 2026
…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>
kriszyp added a commit that referenced this pull request Jul 22, 2026
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>
@kriszyp
kriszyp marked this pull request as ready for review July 22, 2026 03:17
Comment on lines +180 to +202
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, {

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.

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.

kriszyp and others added 3 commits July 22, 2026 13:03
…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>
@kriszyp
kriszyp force-pushed the fix/external-logger-rotation-inheritance branch from 0937fdc to cc374d0 Compare July 22, 2026 19:04
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.

1 participant