diff --git a/TODO b/TODO index db94604..0e0bf86 100644 --- a/TODO +++ b/TODO @@ -140,7 +140,7 @@ Content-Type: application/issue ID: 9 Type: feature Title: implement Manager logger hierarchy -Status: in-progress +Status: done Priority: high Created: 2026-03-13 Relationships: dependsOn:5 diff --git a/src/manager.ts b/src/manager.ts index 1d6828d..ff73f5d 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -79,8 +79,10 @@ export class Manager { if (existing instanceof Placeholder) { rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); + rv.manager = this; this.loggers[scope] = rv; this._fixupChildren(existing, rv); + this._fixupParents(rv); } else { rv = existing; @@ -88,6 +90,7 @@ export class Manager { } else { rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); + rv.manager = this; this.loggers[scope] = rv; this._fixupParents(rv); } @@ -168,7 +171,9 @@ export class Manager { */ public clear() { Object.values(this.loggers).forEach((logger) => { - logger.clear() + if (!(logger instanceof Placeholder)) { + logger.clear(); + } }); } } diff --git a/tests/unit/manager.test.ts b/tests/unit/manager.test.ts index 2df1fbb..43765da 100644 --- a/tests/unit/manager.test.ts +++ b/tests/unit/manager.test.ts @@ -1,7 +1,9 @@ import {expect, jest, test} from '@jest/globals'; import { Manager } from '../../src/manager'; import { Logger, RootLogger } from '../../src/logger'; -import { WARNING, DEBUG, NOTSET } from '../../src/log-level'; +import { Handler } from '../../src/handler'; +import { LogRecord } from '../../src/log-record'; +import { WARNING, DEBUG, INFO, NOTSET } from '../../src/log-level'; function makeManager(): Manager { return new Manager(new RootLogger(WARNING)); @@ -117,6 +119,79 @@ describe('Manager', () => { }); }); + describe('getLogger - manager assignment', () => { + test('sets manager on newly created loggers', () => { + const manager = makeManager(); + const logger = manager.getLogger('app'); + expect(logger.manager).toBe(manager); + }); + + test('sets manager on loggers created from placeholders', () => { + const manager = makeManager(); + manager.getLogger('a.b.c'); + const mid = manager.getLogger('a.b'); + expect(mid.manager).toBe(manager); + }); + + test('isEnabledFor respects manager.disable via getLogger', () => { + const manager = makeManager(); + const logger = manager.getLogger('app'); + logger.setLevel(DEBUG); + + expect(logger.isEnabledFor(DEBUG)).toBe(true); + + manager.disable = WARNING; + logger.clear(); + + expect(logger.isEnabledFor(DEBUG)).toBe(false); + expect(logger.isEnabledFor(WARNING)).toBe(false); + }); + }); + + describe('propagation through hierarchy', () => { + test('child logger propagates records to parent handler', () => { + const manager = makeManager(); + const parent = manager.getLogger('app'); + const child = manager.getLogger('app.module'); + + parent.setLevel(DEBUG); + child.setLevel(DEBUG); + + const emitted: string[] = []; + const handler = new (class extends Handler { + emit(record: LogRecord) { + emitted.push(record.msg); + } + })(DEBUG); + + parent.addHandler(handler); + child.info('propagated message'); + + expect(emitted).toContain('propagated message'); + }); + + test('child does not propagate to unrelated logger', () => { + const manager = makeManager(); + const unrelated = manager.getLogger('other'); + const child = manager.getLogger('app.module'); + + unrelated.setLevel(DEBUG); + child.setLevel(DEBUG); + + const emitted: string[] = []; + const handler = new (class extends Handler { + emit(record: LogRecord) { + emitted.push(record.msg); + } + })(DEBUG); + + unrelated.addHandler(handler); + child.info('should not reach'); + + expect(emitted).not.toContain('should not reach'); + }); + }); + describe('clear', () => { test('clears logger caches', () => { const manager = makeManager(); @@ -124,5 +199,11 @@ describe('Manager', () => { logger.isEnabledFor(DEBUG); manager.clear(); }); + + test('does not throw when placeholders exist', () => { + const manager = makeManager(); + manager.getLogger('a.b.c'); + expect(() => manager.clear()).not.toThrow(); + }); }); });