diff --git a/src/arts/logger/README.md b/src/arts/logger/README.md index 910a652..0e83a06 100644 --- a/src/arts/logger/README.md +++ b/src/arts/logger/README.md @@ -21,7 +21,7 @@ Logtail (Better Stack), Grafana Loki and OpenTelemetry. - **`child(ctx)`** — logger with extended context; shares history and transports. - **`time(label)` / `timeEnd(label)`** — duration measurement with `timer` tag. - **Dynamic transports** — `addTransport()`, `subscribe()`, `removeAllTransports()`. -- **Per-transport filters** — `levels` (per-severity enable + filter) and `filter` (global predicate) at the transport level. The deprecated `minLevel` shorthand was removed; use `levels: levelsAtLeast(LogLevel.X)` for the equivalent effect. +- **Per-transport filters** — `level` (threshold cutoff) and `filter` (arbitrary predicate) at the transport level. Threshold matches the convention used across pino/winston/Python/Java/Go/syslog/OTel. - **Hybrid dispatch** — sync loop, transports may return `Promise` for async work. Caller never awaits. - **Failure routing** — when a transport throws (sync) or rejects (async), a @@ -43,7 +43,7 @@ logr/ ├── transports.ts consoleTransport, httpTransport, callbackTransport ├── source.ts captureSource(), extractError() ├── consts.ts ENGINE_NAME (`engine_logger`), LEVEL_LABELS, LEVEL_CONSOLE_METHOD, -│ ALL_LEVELS, levelsAtLeast, allLevels +│ ALL_LEVELS ├── vitals.ts registerWebVitals(logger, webVitalsSDK, options?) ├── adapters/ │ ├── sentry.ts sentryTransport(sentrySDK, options) @@ -319,18 +319,14 @@ evento. La regla es: - El diagnostic conserva `diagnostics.logger`, asi que sigue siendo un logger normal por debajo. -Los diagnostics pueden activar/desactivar por nivel mediante mapa explicito, no -por comparacion tradicional `level >= minLevel`: +Los diagnostics aceptan un threshold opcional al estilo del logger; si el +descriptor del catalog cae por debajo, se descarta antes de llegar al logger: ```ts const diagnostics = createCatalogDiagnostics({ logger, catalog, - levels: { - [LogLevel.WARN]: { enabled: true }, - [LogLevel.ERROR]: { enabled: true }, - [LogLevel.DEBUG]: { enabled: false } - }, + level: LogLevel.WARN, events: { transport_error: { level: LogLevel.ERROR }, reconnect_exhausted: false @@ -350,9 +346,9 @@ se guarda, pasa filtros de transporte o se manda a Sentry/Datadog/Loki/etc. interface Transport { write(entry: LogEntry): void | Promise; writeBatch?(entries: LogEntry[]): void | Promise; - /** Per-level enable + per-level filter. See `LevelsConfig`. */ - levels?: LevelsConfig; - /** Global predicate applied after `levels`. */ + /** Threshold-style per-transport filter. Entries below `level` are skipped. */ + level?: LogLevel; + /** Predicate applied after the threshold check. */ filter?(entry: LogEntry): boolean; /** Buffer size; entries are queued and flushed on full / interval / page-unload. @default 0 */ buffer?: number; @@ -361,18 +357,9 @@ interface Transport { /** Identifier shown in introspection and in `deniedFor` on failure entries. */ name?: string; } - -type LevelsConfig = Partial< - Record boolean }> ->; - -// Sugar for the most common case "everything from X upwards": -levelsAtLeast(LogLevel.ERROR); -// equivalent to: -// { [LogLevel.ERROR]: { enabled: true }, [LogLevel.FATAL]: { enabled: true } } ``` -- `levels` and `filter` are applied **after** the logger's global level +- `level` and `filter` are applied **after** the logger's global level filter, for fine-grained per-sink routing. - `name` appears in introspection and in `deniedFor` on failure entries. - Sync throws and promise rejections both route to the failure handler. @@ -442,7 +429,7 @@ interface ConsoleTransportOptions { timestamp?: boolean; // @default true prefix?: boolean; // @default true source?: boolean; // @default true — shows "(file.ts:42)" - levels?: LevelsConfig; + level?: LogLevel; // threshold cutoff filter?: (entry: LogEntry) => boolean; // e.g. drop noisy vitals } ``` @@ -450,17 +437,17 @@ interface ConsoleTransportOptions { Browser/Node console with the appropriate method per level (TRACE → debug, FATAL → error). -### `httpTransport({ url, headers?, levels?, filter? })` +### `httpTransport({ url, headers?, level?, filter? })` -Fire-and-forget POST to an arbitrary endpoint. Defaults to -`levelsAtLeast(LogLevel.ERROR)` to stay quiet by default. No retry built in; -batching uses `writeBatch` when `buffer > 0`. +Fire-and-forget POST to an arbitrary endpoint. Defaults to `LogLevel.ERROR` +to stay quiet by default. No retry built in; batching uses `writeBatch` when +`buffer > 0`. ### `callbackTransport(fn, options?)` Factory that builds a `Transport` out of a callback. Structurally equivalent to `logger.subscribe(fn)` but lets you pre-register it in -`LoggerOptions.transports` and configure `levels`, `filter`, `name`. +`LoggerOptions.transports` and configure `level`, `filter`, `name`. --- @@ -509,7 +496,7 @@ datadogLogs.init({ version: '1.0.0' }); -logger.addTransport(datadogTransport(datadogLogs.logger, { levels: levelsAtLeast(LogLevel.WARN) })); +logger.addTransport(datadogTransport(datadogLogs.logger, { level: LogLevel.WARN })); ``` Level mapping: TRACE/DEBUG → `debug`, INFO → `info`, WARN → `warn`, @@ -524,7 +511,7 @@ import { Logtail } from '@logtail/browser'; import { logtailTransport } from '$logger/adapters/logtail'; const lt = new Logtail(''); -logger.addTransport(logtailTransport(lt, { levels: levelsAtLeast(LogLevel.INFO) })); +logger.addTransport(logtailTransport(lt, { level: LogLevel.INFO })); ``` ### Grafana Loki — `lokiTransport(options)` diff --git a/src/arts/logger/adapters/adapters.test.ts b/src/arts/logger/adapters/adapters.test.ts index fc40d53..92eee8d 100644 --- a/src/arts/logger/adapters/adapters.test.ts +++ b/src/arts/logger/adapters/adapters.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { LogLevel, createEngineLogger, levelsAtLeast, type LogEntry } from '../index.ts'; +import { LogLevel, createEngineLogger, type LogEntry } from '../index.ts'; import { sentryTransport, type SentryLike, type SentryScopeLike } from './sentry.ts'; import { datadogTransport, type DatadogLoggerLike } from './datadog.ts'; import { logtailTransport, type LogtailLike } from './logtail.ts'; @@ -173,7 +173,7 @@ describe('datadogTransport', () => { it('maps TRACE/DEBUG to debug, FATAL/ERROR to error', () => { const { dd, calls } = mockDatadog(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(datadogTransport(dd, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(datadogTransport(dd, { level: LogLevel.TRACE })); log.trace('t', 'a'); log.debug('t', 'b'); @@ -188,7 +188,7 @@ describe('datadogTransport', () => { it('preserves original level in messageContext.logLevel', () => { const { dd, calls } = mockDatadog(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(datadogTransport(dd, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(datadogTransport(dd, { level: LogLevel.TRACE })); log.fatal('t', 'boom'); const ctx = calls[0].context as Record; expect(ctx.logLevel).toBe('FATAL'); @@ -197,7 +197,7 @@ describe('datadogTransport', () => { it('uses reserved-safe names (logTags, sourceLocation, durationMs)', () => { const { dd, calls } = mockDatadog(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(datadogTransport(dd, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(datadogTransport(dd, { level: LogLevel.TRACE })); log.info('t', 'hi', { tags: ['x'], traceId: 'r1', @@ -213,7 +213,7 @@ describe('datadogTransport', () => { it('passes reconstructed Error to dd.error', () => { const { dd, calls } = mockDatadog(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(datadogTransport(dd, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(datadogTransport(dd, { level: LogLevel.TRACE })); log.error('t', 'fail', { error: new Error('boom') }); expect(calls[0].error).toBeInstanceOf(Error); @@ -251,7 +251,7 @@ describe('logtailTransport', () => { it('maps levels correctly', () => { const { lt, calls } = mockLogtail(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(logtailTransport(lt, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(logtailTransport(lt, { level: LogLevel.TRACE })); log.trace('t', 'a'); log.debug('t', 'b'); @@ -266,7 +266,7 @@ describe('logtailTransport', () => { it('includes logLevel, category and error in context', () => { const { lt, calls } = mockLogtail(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(logtailTransport(lt, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(logtailTransport(lt, { level: LogLevel.TRACE })); log.error('auth', 'fail', { error: new Error('boom') }); const ctx = calls[0].context as Record; @@ -300,7 +300,7 @@ describe('lokiTransport', () => { lokiTransport({ url: 'https://loki.example.com/loki/api/v1/push', labels: { app: 'test' }, - levels: levelsAtLeast(LogLevel.TRACE) + level: LogLevel.TRACE }) ); @@ -328,7 +328,7 @@ describe('lokiTransport', () => { url: 'https://loki.example.com/push', basicAuthUser: 'user', basicAuthPassword: 'pass', - levels: levelsAtLeast(LogLevel.TRACE) + level: LogLevel.TRACE }) ); @@ -347,7 +347,7 @@ describe('lokiTransport', () => { lokiTransport({ url: 'https://loki.example.com/push', tenantId: 'tenant-1', - levels: levelsAtLeast(LogLevel.TRACE) + level: LogLevel.TRACE }) ); @@ -371,7 +371,7 @@ describe('lokiTransport', () => { } }); log.addTransport( - lokiTransport({ url: 'https://loki.example.com/push', levels: levelsAtLeast(LogLevel.TRACE) }) + lokiTransport({ url: 'https://loki.example.com/push', level: LogLevel.TRACE }) ); log.info('t', 'hi'); @@ -580,7 +580,7 @@ describe('otelTransport', () => { it('maps levels to OTel severityNumber (1/5/9/13/17/21)', () => { const { otelLogger, emitted } = mockOtelLogger(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(otelTransport(otelLogger, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(otelTransport(otelLogger, { level: LogLevel.TRACE })); log.trace('t', 'a'); log.debug('t', 'b'); @@ -595,7 +595,7 @@ describe('otelTransport', () => { it('sets severityText to the level name', () => { const { otelLogger, emitted } = mockOtelLogger(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(otelTransport(otelLogger, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(otelTransport(otelLogger, { level: LogLevel.TRACE })); log.info('t', 'hi'); expect(emitted[0].severityText).toBe('INFO'); }); @@ -603,7 +603,7 @@ describe('otelTransport', () => { it('flattens context as log.context.* attributes', () => { const { otelLogger, emitted } = mockOtelLogger(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(otelTransport(otelLogger, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(otelTransport(otelLogger, { level: LogLevel.TRACE })); log.info('auth', 'hi', { context: { userId: 42, region: 'eu' } }); const attrs = emitted[0].attributes ?? {}; @@ -615,7 +615,7 @@ describe('otelTransport', () => { it('maps error fields to exception.* attributes', () => { const { otelLogger, emitted } = mockOtelLogger(); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); - log.addTransport(otelTransport(otelLogger, { levels: levelsAtLeast(LogLevel.TRACE) })); + log.addTransport(otelTransport(otelLogger, { level: LogLevel.TRACE })); log.error('t', 'fail', { error: new Error('boom') }); const attrs = emitted[0].attributes ?? {}; diff --git a/src/arts/logger/adapters/datadog.ts b/src/arts/logger/adapters/datadog.ts index efd677c..2953c66 100644 --- a/src/arts/logger/adapters/datadog.ts +++ b/src/arts/logger/adapters/datadog.ts @@ -1,6 +1,5 @@ -import type { LevelsConfig, LogEntry, Transport } from '../types.ts'; +import type { LogEntry, Transport } from '../types.ts'; import { LogLevel } from '../types.ts'; -import { levelsAtLeast } from '../consts.ts'; /** * Duck-typed subset of the `@datadog/browser-logs` SDK used by the transport. @@ -19,8 +18,8 @@ export interface DatadogLoggerLike { } export interface DatadogTransportOptions { - /** @default levelsAtLeast(LogLevel.INFO) — INFO/WARN/ERROR/FATAL */ - levels?: LevelsConfig; + /** @default LogLevel.INFO — INFO/WARN/ERROR/FATAL */ + level?: LogLevel; /** @default 'datadog' */ name?: string; filter?: (entry: LogEntry) => boolean; @@ -50,7 +49,7 @@ export interface DatadogTransportOptions { * version: '1.0.0' * }); * - * logger.addTransport(datadogTransport(datadogLogs.logger, { levels: levelsAtLeast(LogLevel.WARN) })); + * logger.addTransport(datadogTransport(datadogLogs.logger, { level: LogLevel.WARN })); */ export function datadogTransport( dd: DatadogLoggerLike, @@ -58,7 +57,7 @@ export function datadogTransport( ): Transport { return { name: options.name ?? 'datadog', - levels: options.levels ?? levelsAtLeast(LogLevel.INFO), + level: options.level ?? LogLevel.INFO, filter: options.filter, write(entry: LogEntry): void { // `source`, `duration`, `tags` are Datadog-reserved attributes — we diff --git a/src/arts/logger/adapters/logtail.ts b/src/arts/logger/adapters/logtail.ts index 9b4a639..34f6519 100644 --- a/src/arts/logger/adapters/logtail.ts +++ b/src/arts/logger/adapters/logtail.ts @@ -1,6 +1,5 @@ -import type { LevelsConfig, LogEntry, Transport } from '../types.ts'; +import type { LogEntry, Transport } from '../types.ts'; import { LogLevel } from '../types.ts'; -import { levelsAtLeast } from '../consts.ts'; /** * Duck-typed subset of the `@logtail/browser` SDK (Better Stack). @@ -18,8 +17,8 @@ export interface LogtailLike { } export interface LogtailTransportOptions { - /** @default levelsAtLeast(LogLevel.INFO) */ - levels?: LevelsConfig; + /** @default LogLevel.INFO */ + level?: LogLevel; /** @default 'logtail' */ name?: string; filter?: (entry: LogEntry) => boolean; @@ -43,7 +42,7 @@ export function logtailTransport( ): Transport { return { name: options.name ?? 'logtail', - levels: options.levels ?? levelsAtLeast(LogLevel.INFO), + level: options.level ?? LogLevel.INFO, filter: options.filter, write(entry: LogEntry): void { const context: Record = { diff --git a/src/arts/logger/adapters/loki.ts b/src/arts/logger/adapters/loki.ts index 7486999..73f4f44 100644 --- a/src/arts/logger/adapters/loki.ts +++ b/src/arts/logger/adapters/loki.ts @@ -1,6 +1,6 @@ -import type { LevelsConfig, LogEntry, Transport } from '../types.ts'; +import type { LogEntry, Transport } from '../types.ts'; import { LogLevel } from '../types.ts'; -import { LEVEL_LABELS, levelsAtLeast } from '../consts.ts'; +import { LEVEL_LABELS } from '../consts.ts'; import { LoggerLokiPushFailedError } from '../errors.ts'; export interface LokiTransportOptions { @@ -19,8 +19,8 @@ export interface LokiTransportOptions { /** Basic auth user. Equivalent to `Authorization: Basic base64(user:pass)`. */ basicAuthUser?: string; basicAuthPassword?: string; - /** @default levelsAtLeast(LogLevel.INFO) */ - levels?: LevelsConfig; + /** @default LogLevel.INFO */ + level?: LogLevel; /** @default 'loki' */ name?: string; filter?: (entry: LogEntry) => boolean; @@ -101,7 +101,7 @@ export function lokiTransport(options: LokiTransportOptions): Transport { return { name: options.name ?? 'loki', - levels: options.levels ?? levelsAtLeast(LogLevel.INFO), + level: options.level ?? LogLevel.INFO, filter: options.filter, async write(entry: LogEntry): Promise { const streamLabels: Record = { diff --git a/src/arts/logger/adapters/otel.ts b/src/arts/logger/adapters/otel.ts index be91ce4..d1bd61f 100644 --- a/src/arts/logger/adapters/otel.ts +++ b/src/arts/logger/adapters/otel.ts @@ -1,6 +1,5 @@ -import type { LevelsConfig, LogEntry, Transport } from '../types.ts'; +import type { LogEntry, Transport } from '../types.ts'; import { LogLevel } from '../types.ts'; -import { levelsAtLeast } from '../consts.ts'; /** * Duck-typed subset of the OpenTelemetry Logs API. The caller injects an @@ -30,8 +29,8 @@ export interface OtelLogRecord { } export interface OtelTransportOptions { - /** @default levelsAtLeast(LogLevel.INFO) */ - levels?: LevelsConfig; + /** @default LogLevel.INFO */ + level?: LogLevel; /** @default 'otel' */ name?: string; filter?: (entry: LogEntry) => boolean; @@ -58,7 +57,7 @@ export function otelTransport( ): Transport { return { name: options.name ?? 'otel', - levels: options.levels ?? levelsAtLeast(LogLevel.INFO), + level: options.level ?? LogLevel.INFO, filter: options.filter, write(entry: LogEntry): void { const attributes: Record = { diff --git a/src/arts/logger/adapters/sentry.ts b/src/arts/logger/adapters/sentry.ts index e98d00a..d41d93d 100644 --- a/src/arts/logger/adapters/sentry.ts +++ b/src/arts/logger/adapters/sentry.ts @@ -1,6 +1,5 @@ -import type { LevelsConfig, LogEntry, Transport } from '../types.ts'; +import type { LogEntry, Transport } from '../types.ts'; import { LogLevel } from '../types.ts'; -import { levelsAtLeast } from '../consts.ts'; /** * Duck-typed subset of the Sentry SDK used by the transport. The `engine_logger` @@ -43,11 +42,11 @@ export interface SentryTransportOptions { */ breadcrumbLevel?: LogLevel; /** - * Optional per-level acceptance override. When undefined, the transport - * accepts `[breadcrumbLevel, FATAL]` — everything required for its dual - * event/breadcrumb routing. Set this to restrict further. + * Optional threshold-style acceptance override. When undefined, the + * transport accepts `[breadcrumbLevel, FATAL]` — everything required for + * its dual event/breadcrumb routing. Set this to restrict further. */ - levels?: LevelsConfig; + level?: LogLevel; /** Transport name shown in introspection and `deniedFor`. @default 'sentry' */ name?: string; /** Extra predicate. When it returns `false`, the entry is skipped entirely. */ @@ -88,7 +87,7 @@ export function sentryTransport( name, // Accept everything from breadcrumbLevel upwards; the write() body // decides whether to emit an event or a breadcrumb. - levels: options.levels ?? levelsAtLeast(breadcrumbLevel), + level: options.level ?? breadcrumbLevel, filter: options.filter, write(entry: LogEntry): void { if (entry.level < breadcrumbLevel) return; diff --git a/src/arts/logger/consts.ts b/src/arts/logger/consts.ts index 9cdfd43..9dffd41 100644 --- a/src/arts/logger/consts.ts +++ b/src/arts/logger/consts.ts @@ -1,5 +1,4 @@ import { LogLevel } from './types.ts'; -import type { LevelsConfig } from './types.ts'; export const LOGGER_MODULE = 'logger'; @@ -29,35 +28,6 @@ export const ALL_LEVELS: readonly LogLevel[] = [ LogLevel.FATAL ]; -/** - * Build a `LevelsConfig` that enables every level at or above `min`. - * Sugar for the most common acceptance pattern. - * - * @example - * addTransport({ - * name: 'sentry', - * levels: levelsAtLeast(LogLevel.ERROR), // ERROR + FATAL - * write: ... - * }); - */ -export function levelsAtLeast(min: LogLevel): LevelsConfig { - const record: LevelsConfig = {}; - for (const l of ALL_LEVELS) { - if (l >= min) record[l] = { enabled: true }; - } - return record; -} - -/** - * Build a `LevelsConfig` that enables every level. Handy as an explicit - * "accept everything" override when some other transport restricts levels. - */ -export function allLevels(): LevelsConfig { - const record: LevelsConfig = {}; - for (const l of ALL_LEVELS) record[l] = { enabled: true }; - return record; -} - /** * Canonical label per level. Used by `vitals` for category routing and by * external adapters (e.g. Loki) that ship the level as a string label. diff --git a/src/arts/logger/engine-logger.ts b/src/arts/logger/engine-logger.ts index 696ee6f..a4889f4 100644 --- a/src/arts/logger/engine-logger.ts +++ b/src/arts/logger/engine-logger.ts @@ -11,8 +11,8 @@ import type { LogSource, LogError } from './types.ts'; -import { LogLevel, type LevelsConfig } from './types.ts'; -import { ALL_LEVELS, ENGINE_NAME } from './consts.ts'; +import { LogLevel } from './types.ts'; +import { ENGINE_NAME } from './consts.ts'; import { isDev as DEV } from '$libs/env'; import { consoleTransport } from './transports.ts'; import { captureSource, extractError } from './source.ts'; @@ -47,7 +47,7 @@ function generateId(): string { * - `child(ctx)` — child logger with extended context, shares history/transports. * - `time(label)` / `timeEnd(label)` — duration measurement. * - `addTransport(t)` / `subscribe(fn)` — dynamic bus (Sentry, metrics, etc.). - * - Per-transport `filter` and `levels` (via `levelsAtLeast(LogLevel.X)`) — e.g. Sentry only ERROR+. + * - Per-transport `level` (threshold) and `filter` — e.g. Sentry only ERROR+. * - Lazy messages via thunks — not evaluated if filtered by global level. * * @example @@ -60,7 +60,7 @@ function generateId(): string { * // Sentry as a dynamic transport * const detachSentry = log.addTransport({ * name: 'sentry', - * levels: levelsAtLeast(LogLevel.ERROR), + * level: LogLevel.ERROR, * write: (entry) => { * if (entry.error) Sentry.captureException(entry.error); * else Sentry.captureMessage(entry.message); @@ -84,19 +84,10 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger { const buffers = new WeakMap(); const flushTimers = new WeakMap>(); - // 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); + // Mutable state shared between root logger and its children. Entries + // below `level` are dropped before they reach history or transports. const state = { - level: initialLevel, - enabledLevels: initialEnabled, + level: options.level ?? LogLevel.WARN, maxLogs: options.maxLogs ?? 1000, entries: [] as LogEntry[], transports: [...(options.transports ?? [consoleTransport()])] @@ -132,36 +123,11 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger { interface LoggerState { level: LogLevel; - enabledLevels: Set; maxLogs: number; entries: LogEntry[]; transports: Transport[]; } -/** - * Convert a threshold `LogLevel` into the equivalent `Set` - * of enabled levels. A threshold of WARN enables WARN + ERROR + FATAL. - */ -function buildEnabledLevelsFromThreshold(level: LogLevel): Set { - const result = new Set(); - for (const l of ALL_LEVELS) { - if (l >= level) result.add(l); - } - return result; -} - -/** - * Project a `LevelsConfig` (per-level enable map) into the - * `Set` the engine consults at every log site. - */ -function buildEnabledLevelsFromLevelsConfig(levels: LevelsConfig): Set { - const result = new Set(); - for (const l of ALL_LEVELS) { - if (levels[l]?.enabled) result.add(l); - } - return result; -} - interface DispatchContext { deniedFor: Set; } @@ -265,7 +231,7 @@ function buildLogger( skipFrames: number ): void { // Global level check first: if dropped, skip thunk evaluation and source capture. - if (!state.enabledLevels.has(lvl)) return; + if (lvl < state.level) return; const resolved = resolveMessage(message); const source: LogSource | undefined = @@ -331,19 +297,13 @@ function buildLogger( } /** - * Decide whether the transport accepts an entry at its level, using the - * per-level `levels` config: - * - no `levels` set → all levels pass - * - level missing or `enabled: false` → drop - * - `enabled: true` → apply the optional per-level filter + * Decide whether the transport accepts an entry at its level. When + * `transport.level` is undefined the entry passes; otherwise the entry + * is dropped if `entry.level < transport.level`. */ function transportAcceptsLevel(transport: Transport, entry: LogEntry): boolean { - const cfg = transport.levels; - if (!cfg) return true; - const entryCfg = cfg[entry.level]; - if (!entryCfg || !entryCfg.enabled) return false; - if (entryCfg.filter && !entryCfg.filter(entry)) return false; - return true; + if (transport.level === undefined) return true; + return entry.level >= transport.level; } function writeOne(transport: Transport, entry: LogEntry, context: DispatchContext): void { @@ -566,7 +526,6 @@ function buildLogger( setLevel(l: LogLevel): void { state.level = l; - state.enabledLevels = buildEnabledLevelsFromThreshold(l); }, setMaxLogs(max: number): void { diff --git a/src/arts/logger/index.ts b/src/arts/logger/index.ts index 03b6c46..4cf499e 100644 --- a/src/arts/logger/index.ts +++ b/src/arts/logger/index.ts @@ -7,8 +7,6 @@ export { ENGINE_NAME, ALL_LEVELS, - levelsAtLeast, - allLevels, LEVEL_LABELS, LEVEL_CONSOLE_METHOD } from './consts.ts'; @@ -29,7 +27,6 @@ export type { LogInput, LogEntry, LogFilters, - LevelsConfig, Transport, SubscriberFn, ConsoleTransportOptions, diff --git a/src/arts/logger/test/logr.test.ts b/src/arts/logger/test/logr.test.ts index 748d227..e4306cf 100644 --- a/src/arts/logger/test/logr.test.ts +++ b/src/arts/logger/test/logr.test.ts @@ -29,7 +29,7 @@ import { type Transport } from '../index.ts'; import { extractError } from '../source.ts'; -import { ENGINE_NAME as LOGGER_NAME, levelsAtLeast } from '../consts.ts'; +import { ENGINE_NAME as LOGGER_NAME } from '../consts.ts'; function capture(): { transport: Transport; entries: LogEntry[] } { const entries: LogEntry[] = []; @@ -91,45 +91,6 @@ 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', () => { @@ -468,13 +429,13 @@ describe('createEngineLogger — dynamic transports', () => { }); describe('createEngineLogger — per-transport levels and filter', () => { - it('drops entries below transport.levels acceptance', () => { + it('drops entries below transport.level threshold', () => { const entries: LogEntry[] = []; const log = createEngineLogger({ level: LogLevel.TRACE }); log.removeAllTransports(); log.addTransport({ name: 't', - levels: levelsAtLeast(LogLevel.WARN), + level: LogLevel.WARN, write: (e) => { entries.push(e); } @@ -650,9 +611,9 @@ describe('callbackTransport', () => { expect(hits).toHaveLength(1); }); - it('respects levels option', () => { + it('respects level option', () => { const hits: LogEntry[] = []; - const cb = callbackTransport((e) => hits.push(e), { levels: levelsAtLeast(LogLevel.ERROR) }); + const cb = callbackTransport((e) => hits.push(e), { level: LogLevel.ERROR }); const log = createEngineLogger({ level: LogLevel.TRACE, transports: [cb] diff --git a/src/arts/logger/transports.ts b/src/arts/logger/transports.ts index 3ea64d9..dc29fa4 100644 --- a/src/arts/logger/transports.ts +++ b/src/arts/logger/transports.ts @@ -6,7 +6,7 @@ import type { } from './types.ts'; import { LogLevel } from './types.ts'; -import { LEVEL_CONSOLE_METHOD, levelsAtLeast } from './consts.ts'; +import { LEVEL_CONSOLE_METHOD } from './consts.ts'; import { LoggerHttpTransportPushFailedError } from './errors.ts'; // ============================================================================ @@ -22,20 +22,20 @@ import { LoggerHttpTransportPushFailedError } from './errors.ts'; * @example * consoleTransport() * consoleTransport({ timestamp: false, prefix: false }) - * consoleTransport({ levels: levelsAtLeast(LogLevel.INFO) }) + * consoleTransport({ level: LogLevel.INFO }) */ export function consoleTransport(options: ConsoleTransportOptions = {}): Transport { const { timestamp: showTimestamp = true, prefix: showPrefix = true, source: showSource = true, - levels, + level, filter } = options; return { name: 'console', - levels, + level, filter, write(entry: LogEntry): void { const args: unknown[] = []; @@ -111,7 +111,7 @@ export function httpTransport(options: HttpTransportOptions): Transport { return { name: 'http', - levels: options.levels ?? levelsAtLeast(LogLevel.ERROR), + level: options.level ?? LogLevel.ERROR, filter: options.filter, async write(entry: LogEntry): Promise { await post(serialize(entry)); @@ -131,7 +131,7 @@ export function httpTransport(options: HttpTransportOptions): Transport { * Transport that invokes a callback for each reaching entry. Structurally * equivalent to `logger.subscribe(fn)` — it exists as a factory so it can be * pre-registered via `LoggerOptions.transports` or composed with - * `levels`/`filter`/`name`. + * `level`/`filter`/`name`. * * @example * callbackTransport((entry) => captured.push(entry)) @@ -143,20 +143,20 @@ export function httpTransport(options: HttpTransportOptions): Transport { * if (entry.error) Sentry.captureException(entry.error); * else Sentry.captureMessage(entry.message); * }, - * { levels: levelsAtLeast(LogLevel.ERROR), name: 'sentry' } + * { level: LogLevel.ERROR, name: 'sentry' } * ) */ export function callbackTransport( fn: (entry: LogEntry) => void, options: { - levels?: import('./types.ts').LevelsConfig; + level?: LogLevel; filter?: (entry: LogEntry) => boolean; name?: string; } = {} ): Transport { return { write: fn, - levels: options.levels, + level: options.level, filter: options.filter, name: options.name ?? 'callback' }; diff --git a/src/arts/logger/types.ts b/src/arts/logger/types.ts index 7c031ca..81c2e4b 100644 --- a/src/arts/logger/types.ts +++ b/src/arts/logger/types.ts @@ -65,10 +65,7 @@ export interface LogEntry { * `getLogs()` filters. All optional — combined with AND. * * - `level` matches the level exactly. - * - `fromLevel` matches every level at or above the given threshold (mirrors - * the `levelsAtLeast(min)` helper used on transports). Distinct vocabulary - * from the rejected per-transport `minLevel` shorthand — `LogFilters` is - * for querying history, transports use `levels` for routing. + * - `fromLevel` matches every level at or above the given threshold. */ export interface LogFilters { level?: LogLevel; @@ -90,37 +87,10 @@ export interface LogFilters { * Can be registered at logger creation (`options.transports`) or dynamically * via `logger.addTransport()` / `logger.subscribe()`. * - * Each transport decides whether to process each entry via `levels` (per- - * severity acceptance + per-level filter) and `filter` (global predicate) — - * both applied after the logger's global level filter. + * Each transport decides whether to process each entry via `level` (threshold + * cutoff) and `filter` (arbitrary predicate) — both applied after the logger's + * global level filter. */ -/** - * Per-level acceptance + filter for a transport. More flexible than a simple - * min-level cutoff: you can enable individual levels, skip specific ones in - * the middle, and attach a filter only to certain levels. - * - * - Undefined key → disabled (the entry is skipped). - * - `enabled: false` → also skipped. - * - `enabled: true` with no `filter` → pass every entry of that level. - * - `enabled: true` with `filter` → pass only entries for which the filter - * returns `true`. - * - * @example - * // Sentry: only ERROR and FATAL - * { [LogLevel.ERROR]: { enabled: true }, [LogLevel.FATAL]: { enabled: true } } - * - * @example - * // INFO limited to auth category, WARN passes everything, ERROR too - * { - * [LogLevel.INFO]: { enabled: true, filter: (e) => e.category === 'auth' }, - * [LogLevel.WARN]: { enabled: true }, - * [LogLevel.ERROR]: { enabled: true } - * } - */ -export type LevelsConfig = Partial< - Record boolean }> ->; - export interface Transport { /** * Process a single entry. Sync or async. @@ -159,17 +129,16 @@ export interface Transport { */ flushIntervalMs?: number; /** - * Per-level acceptance config. When undefined, every level passes (subject - * only to the logger's global level filter). See `LevelsConfig`. - * - * Use `levelsAtLeast(LogLevel.X)` as a shorthand for the common - * "everything from level X and above" case. + * Threshold-style per-transport filter. Entries with `entry.level < level` + * are skipped by this transport (the engine still records them in history + * if they passed the global level). When undefined, every entry that + * reaches the transport is accepted. */ - levels?: LevelsConfig; + level?: LogLevel; /** - * Global arbitrary predicate applied after the per-level filter. If it - * returns `false`, the entry is skipped. Useful to drop a category/tag - * regardless of level. + * Arbitrary predicate applied after the threshold check. If it returns + * `false`, the entry is skipped. Useful to drop a category/tag regardless + * of level. */ filter?(entry: LogEntry): boolean; /** @@ -215,7 +184,8 @@ export interface ConsoleTransportOptions { prefix?: boolean; /** Include the `source` in parentheses. @default true */ source?: boolean; - levels?: LevelsConfig; + /** Threshold-style per-transport filter. */ + level?: LogLevel; /** * Predicate that skips noisy entries (vitals, high-volume debug streams) * without affecting other transports. Returns `false` to drop the entry @@ -231,8 +201,8 @@ export interface ConsoleTransportOptions { export interface HttpTransportOptions { url: string; headers?: Record; - /** @default levelsAtLeast(LogLevel.ERROR) */ - levels?: LevelsConfig; + /** @default LogLevel.ERROR */ + level?: LogLevel; filter?: (entry: LogEntry) => boolean; } @@ -242,22 +212,11 @@ export interface HttpTransportOptions { export interface LoggerOptions { /** - * Threshold-style filter. Equivalent to `levels: levelsAtLeast(level)`. - * Provided for ergonomics; `levels` takes precedence when both are set. + * Threshold-style global filter. Entries with `entry.level < level` are + * dropped before they reach history or transports. * @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. */ diff --git a/src/libs/logger/diagnostics.ts b/src/libs/logger/diagnostics.ts index 16b065f..2a9b53c 100644 --- a/src/libs/logger/diagnostics.ts +++ b/src/libs/logger/diagnostics.ts @@ -3,14 +3,11 @@ import type { CatalogDiagnosticsOptions, DiagnosticCatalogEntry, DiagnosticEvent, - DiagnosticEventConfig, DiagnosticLogDescriptor, - DiagnosticLogEntry, Diagnostics, LoggerDiagnosticsOptions, Logger, - LogInput, - LevelConfig + LogInput } from './types.ts'; const DIAGNOSTIC_LOGGER_METHODS: Readonly< @@ -75,17 +72,6 @@ function resolveCatalogEntry( return entry; } -function logLevelEnabled( - levels: LevelConfig | undefined, - level: LogLevel, - entry: DiagnosticLogEntry -): boolean { - if (levels === undefined) return true; - const config = levels[level]; - if (config === undefined || config.enabled === false) return false; - return config.filter?.(entry) ?? true; -} - function resolveDiagnosticOverride( options: CatalogDiagnosticsOptions, event: TEvent @@ -149,15 +135,7 @@ export function createCatalogDiagnostics( const level = override?.level ?? descriptor.level; const category = descriptor.category ?? event.scope ?? options.defaultCategory ?? event.artifact; - const diagnosticEntry: DiagnosticLogEntry = { - level, - category, - message: descriptor.message, - tags: descriptor.tags - }; - if (!logLevelEnabled(options.levels, level, diagnosticEntry)) { - return undefined; - } + if (options.level !== undefined && level < options.level) return undefined; return { ...descriptor, level, diff --git a/src/libs/logger/index.ts b/src/libs/logger/index.ts index f82b17b..594ac84 100644 --- a/src/libs/logger/index.ts +++ b/src/libs/logger/index.ts @@ -10,7 +10,6 @@ export type { DiagnosticLogEntry, Diagnostics, LoggerDiagnosticsOptions, - LevelConfig, Logger, LogFn, LogInput, diff --git a/src/libs/logger/types.ts b/src/libs/logger/types.ts index f5fe370..24ffe3e 100644 --- a/src/libs/logger/types.ts +++ b/src/libs/logger/types.ts @@ -8,10 +8,6 @@ export enum LogLevel { NONE = 999 } -export type LevelConfig = Partial< - Record boolean }> ->; - export type LogMessage = string | (() => string); export interface LogSource { @@ -91,6 +87,12 @@ export interface CatalogDiagnosticsOptions { readonly logger?: Logger; readonly catalog: DiagnosticCatalog; readonly defaultCategory?: string; - readonly levels?: LevelConfig; + /** + * Threshold-style filter — entries below this level are dropped before the + * underlying logger is called. Defaults to no extra threshold (the logger's + * own level filter still applies). Use `LogLevel.NONE` to silence the + * diagnostics layer entirely. + */ + readonly level?: LogLevel; readonly events?: Partial>; } diff --git a/src/web/routes/active/_data/artifact-docs.ts b/src/web/routes/active/_data/artifact-docs.ts index 0788ebe..1dbd5e4 100644 --- a/src/web/routes/active/_data/artifact-docs.ts +++ b/src/web/routes/active/_data/artifact-docs.ts @@ -3533,14 +3533,14 @@ const result = await Sium.validate(ProfileSchema, formValue);` ], quickStart: { title: 'Create logger', - code: `import { createEngineLogger, consoleTransport, levelsAtLeast, LogLevel } from '$logger'; + code: `import { createEngineLogger, consoleTransport, LogLevel } from '$logger'; import { sentryTransport } from '$logger/adapters/sentry'; const Logger = createEngineLogger({ level: LogLevel.INFO, transports: [ consoleTransport(), - sentryTransport(Sentry, { levels: levelsAtLeast(LogLevel.ERROR) }) + sentryTransport(Sentry, { level: LogLevel.ERROR }) ], globalContext: { appVersion: '1.0.0' } }); diff --git a/src/web/routes/test/logr/+page.svelte b/src/web/routes/test/logr/+page.svelte index d1d44f1..07d3a92 100644 --- a/src/web/routes/test/logr/+page.svelte +++ b/src/web/routes/test/logr/+page.svelte @@ -4,8 +4,7 @@ LogLevel, callbackTransport, consoleTransport, - createEngineLogger, - levelsAtLeast + createEngineLogger } from '$logger'; import { sentryTransport } from '$logger/adapters/sentry'; import { registerWebVitals } from '$logger/vitals'; @@ -32,7 +31,7 @@ // Filter vitals out of the console to keep it readable — they // still flow to Sentry/Datadog/Loki/Capture. consoleTransport({ - levels: levelsAtLeast(LogLevel.DEBUG), + level: LogLevel.DEBUG, filter: (e) => !e.tags?.includes('vitals') }), captureTransport @@ -127,7 +126,7 @@ function attachFakeSentry(): void { const fakeSentry: Transport = { name: 'fake-sentry', - levels: levelsAtLeast(LogLevel.ERROR), + level: LogLevel.ERROR, write(entry) { console.log('[fake-sentry] captureException/Message', { message: entry.message, @@ -496,8 +495,8 @@ {#each activeTransports as t, i (i)}
{t.name ?? 'anonymous'} - {#if t.levels} - levels: {Object.keys(t.levels).map((k) => levelName(Number(k))).join(', ')} + {#if t.level !== undefined} + level: {levelName(t.level)}+ {/if} {#if t.filter} filter