Bloque C5 — logger gates by per-level enablement at the engine level

Audit P1/P2: transports already accepted `LevelsConfig`
(`{ [LogLevel.WARN]: { enabled: true } }`), but the engine itself
gated entries with a threshold (`if (lvl < state.level) return`).
Two filtering vocabularies for the same vocabulary; a 1.0 contract
should pick one.

Decision: align the engine on per-level enablement (the same
shape transports use). Threshold semantics stay as a shorthand —
`level: LogLevel.WARN` is equivalent to
`levels: levelsAtLeast(LogLevel.WARN)`. When both are set, `levels`
wins. Backward-compatible: existing apps that only pass `level`
get the exact same enabled set as before because the engine
projects the threshold into `enabledLevels` at boot.

Implementation:
- `LoggerOptions` gains `levels?: LevelsConfig`. Two helpers,
  `buildEnabledLevelsFromThreshold(level)` and
  `buildEnabledLevelsFromLevelsConfig(levels)`, project either form
  into the runtime `Set<LogLevel>` the engine consults at the log
  site.
- Engine state grows `enabledLevels: Set<LogLevel>`. The hot path
  becomes `if (!state.enabledLevels.has(lvl)) return`.
- `setLevel(level)` keeps working — it rebuilds `enabledLevels`
  from the new threshold.

Two regression tests pin the new behaviour: arbitrary subset via
`levelsAtLeast(LogLevel.WARN)`, and `levels` overriding `level`
when both are set.

Suite: 1515 / 1515 (+3 tests across logger and storage clock).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 6b6a96fb11
commit 3567206fa3

@ -11,8 +11,8 @@ import type {
LogSource,
LogError
} from './types.ts';
import { LogLevel } from './types.ts';
import { ENGINE_NAME } from './consts.ts';
import { LogLevel, type LevelsConfig } from './types.ts';
import { ALL_LEVELS, ENGINE_NAME } from './consts.ts';
import { isDev as DEV } from '$libs/env';
import { consoleTransport } from './transports.ts';
import { captureSource, extractError } from './source.ts';
@ -84,9 +84,19 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger {
const buffers = new WeakMap<Transport, BufferedLogEntry[]>();
const flushTimers = new WeakMap<Transport, ReturnType<typeof setTimeout>>();
// Mutable state shared between root logger and its children.
// Mutable state shared between root logger and its children. The
// engine gates entries by per-level enablement (`enabledLevels`), the
// same vocabulary transports use. `level` (threshold) is preserved
// as a shorthand: when only `level` is given, we derive the levels
// set from it; when `levels` is given explicitly, it wins.
const initialLevel = options.level ?? LogLevel.WARN;
const initialEnabled =
options.levels !== undefined
? buildEnabledLevelsFromLevelsConfig(options.levels)
: buildEnabledLevelsFromThreshold(initialLevel);
const state = {
level: options.level ?? LogLevel.WARN,
level: initialLevel,
enabledLevels: initialEnabled,
maxLogs: options.maxLogs ?? 1000,
entries: [] as LogEntry[],
transports: [...(options.transports ?? [consoleTransport()])]
@ -122,11 +132,36 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger {
interface LoggerState {
level: LogLevel;
enabledLevels: Set<LogLevel>;
maxLogs: number;
entries: LogEntry[];
transports: Transport[];
}
/**
* Convert a threshold `LogLevel` into the equivalent `Set<LogLevel>`
* of enabled levels. A threshold of WARN enables WARN + ERROR + FATAL.
*/
function buildEnabledLevelsFromThreshold(level: LogLevel): Set<LogLevel> {
const result = new Set<LogLevel>();
for (const l of ALL_LEVELS) {
if (l >= level) result.add(l);
}
return result;
}
/**
* Project a `LevelsConfig` (per-level enable map) into the
* `Set<LogLevel>` the engine consults at every log site.
*/
function buildEnabledLevelsFromLevelsConfig(levels: LevelsConfig): Set<LogLevel> {
const result = new Set<LogLevel>();
for (const l of ALL_LEVELS) {
if (levels[l]?.enabled) result.add(l);
}
return result;
}
interface DispatchContext {
deniedFor: Set<string>;
}
@ -230,7 +265,7 @@ function buildLogger(
skipFrames: number
): void {
// Global level check first: if dropped, skip thunk evaluation and source capture.
if (lvl < state.level) return;
if (!state.enabledLevels.has(lvl)) return;
const resolved = resolveMessage(message);
const source: LogSource | undefined =
@ -531,6 +566,7 @@ function buildLogger(
setLevel(l: LogLevel): void {
state.level = l;
state.enabledLevels = buildEnabledLevelsFromThreshold(l);
},
setMaxLogs(max: number): void {

@ -90,6 +90,46 @@ describe('createEngineLogger — level methods', () => {
expect(entries.map((e) => e.message)).toEqual(['one', 'two']);
});
it('honors `levels` (per-level enable map) at the global filter', async () => {
const { transport, entries } = capture();
// Allow only WARN + ERROR + FATAL — explicitly disable INFO
// (would otherwise be enabled by a threshold-style filter at WARN
// which we are NOT using here).
const { levelsAtLeast } = await import('../consts.ts');
const log = createEngineLogger({
levels: levelsAtLeast(LogLevel.WARN),
transports: [transport]
});
log.trace('t', 'drop-trace');
log.debug('t', 'drop-debug');
log.info('t', 'drop-info');
log.warn('t', 'keep-warn');
log.error('t', 'keep-error');
log.fatal('t', 'keep-fatal');
expect(entries.map((e) => e.message)).toEqual(['keep-warn', 'keep-error', 'keep-fatal']);
});
it('lets `levels` win over `level` when both are set', () => {
const { transport, entries } = capture();
// `level: TRACE` would normally accept everything; `levels` only
// enables ERROR + FATAL.
const log = createEngineLogger({
level: LogLevel.TRACE,
levels: {
[LogLevel.ERROR]: { enabled: true },
[LogLevel.FATAL]: { enabled: true }
},
transports: [transport]
});
log.info('t', 'drop');
log.error('t', 'keep');
expect(entries.map((e) => e.message)).toEqual(['keep']);
});
});
describe('createEngineLogger — lazy messages', () => {

@ -241,8 +241,23 @@ export interface HttpTransportOptions {
// ============================================================================
export interface LoggerOptions {
/** @default LogLevel.WARN */
/**
* Threshold-style filter. Equivalent to `levels: levelsAtLeast(level)`.
* Provided for ergonomics; `levels` takes precedence when both are set.
* @default LogLevel.WARN
*/
level?: LogLevel;
/**
* Per-level enablement map at the logger level — same shape transports
* use. When omitted, the engine derives it from `level` (threshold) so
* existing apps keep working unchanged. Use `levelsAtLeast(level)` to
* convert a threshold; pass an explicit `LevelsConfig` to enable
* arbitrary subsets (e.g. only WARN + ERROR + FATAL, skipping INFO).
*
* Aligns the engine's global filter with the per-transport filter so
* the same vocabulary works at both layers.
*/
levels?: LevelsConfig;
/** @default 1000 */
maxLogs?: number;
/** Transports registered at creation time. More can be added dynamically. */

Loading…
Cancel
Save

Powered by TurnKey Linux.