Bloque C5 (revert) — logger back to threshold-only filtering

Reverts the per-level enable map (`LevelsConfig` / `LevelConfig`) added in
3567206 and aligns every layer with threshold semantics — the unwritten
convention used by pino/winston/Python/Java/Go/syslog/OpenTelemetry.

- `arts/logger/types.ts`: drop `LevelsConfig`. `Transport.level?: LogLevel`
  replaces `Transport.levels?`. Same shape on console/http/callback options.
- `arts/logger/engine-logger.ts`: restore `if (lvl < state.level) return`
  gate; remove `enabledLevels` Set and the build-from-* helpers.
- `arts/logger/consts.ts`: remove `levelsAtLeast` / `allLevels`. Keep
  `ALL_LEVELS` for adapters that iterate severities.
- 5 SDK adapters (datadog/logtail/loki/otel/sentry): `level?: LogLevel`
  with the same defaults as before; per-level filter dropped because
  every adapter already exposes a top-level `filter?` predicate that
  serves the rare advanced case without losing capability.
- `libs/logger`: `CatalogDiagnosticsOptions.level?: LogLevel` replaces
  `levels?: LevelConfig<DiagnosticLogEntry>`; `LevelConfig` removed from
  the public surface.
- Test page and artifact-docs example updated to threshold form.

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

@ -21,7 +21,7 @@ Logtail (Better Stack), Grafana Loki and OpenTelemetry.
- **`child(ctx)`** — logger with extended context; shares history and transports. - **`child(ctx)`** — logger with extended context; shares history and transports.
- **`time(label)` / `timeEnd(label)`** — duration measurement with `timer` tag. - **`time(label)` / `timeEnd(label)`** — duration measurement with `timer` tag.
- **Dynamic transports** — `addTransport()`, `subscribe()`, `removeAllTransports()`. - **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<void>` for - **Hybrid dispatch** — sync loop, transports may return `Promise<void>` for
async work. Caller never awaits. async work. Caller never awaits.
- **Failure routing** — when a transport throws (sync) or rejects (async), a - **Failure routing** — when a transport throws (sync) or rejects (async), a
@ -43,7 +43,7 @@ logr/
├── transports.ts consoleTransport, httpTransport, callbackTransport ├── transports.ts consoleTransport, httpTransport, callbackTransport
├── source.ts captureSource(), extractError() ├── source.ts captureSource(), extractError()
├── consts.ts ENGINE_NAME (`engine_logger`), LEVEL_LABELS, LEVEL_CONSOLE_METHOD, ├── consts.ts ENGINE_NAME (`engine_logger`), LEVEL_LABELS, LEVEL_CONSOLE_METHOD,
│ ALL_LEVELS, levelsAtLeast, allLevels │ ALL_LEVELS
├── vitals.ts registerWebVitals(logger, webVitalsSDK, options?) ├── vitals.ts registerWebVitals(logger, webVitalsSDK, options?)
├── adapters/ ├── adapters/
│ ├── sentry.ts sentryTransport(sentrySDK, options) │ ├── sentry.ts sentryTransport(sentrySDK, options)
@ -319,18 +319,14 @@ evento. La regla es:
- El diagnostic conserva `diagnostics.logger`, asi que sigue siendo un logger - El diagnostic conserva `diagnostics.logger`, asi que sigue siendo un logger
normal por debajo. normal por debajo.
Los diagnostics pueden activar/desactivar por nivel mediante mapa explicito, no Los diagnostics aceptan un threshold opcional al estilo del logger; si el
por comparacion tradicional `level >= minLevel`: descriptor del catalog cae por debajo, se descarta antes de llegar al logger:
```ts ```ts
const diagnostics = createCatalogDiagnostics({ const diagnostics = createCatalogDiagnostics({
logger, logger,
catalog, catalog,
levels: { level: LogLevel.WARN,
[LogLevel.WARN]: { enabled: true },
[LogLevel.ERROR]: { enabled: true },
[LogLevel.DEBUG]: { enabled: false }
},
events: { events: {
transport_error: { level: LogLevel.ERROR }, transport_error: { level: LogLevel.ERROR },
reconnect_exhausted: false reconnect_exhausted: false
@ -350,9 +346,9 @@ se guarda, pasa filtros de transporte o se manda a Sentry/Datadog/Loki/etc.
interface Transport { interface Transport {
write(entry: LogEntry): void | Promise<void>; write(entry: LogEntry): void | Promise<void>;
writeBatch?(entries: LogEntry[]): void | Promise<void>; writeBatch?(entries: LogEntry[]): void | Promise<void>;
/** Per-level enable + per-level filter. See `LevelsConfig`. */ /** Threshold-style per-transport filter. Entries below `level` are skipped. */
levels?: LevelsConfig; level?: LogLevel;
/** Global predicate applied after `levels`. */ /** Predicate applied after the threshold check. */
filter?(entry: LogEntry): boolean; filter?(entry: LogEntry): boolean;
/** Buffer size; entries are queued and flushed on full / interval / page-unload. @default 0 */ /** Buffer size; entries are queued and flushed on full / interval / page-unload. @default 0 */
buffer?: number; buffer?: number;
@ -361,18 +357,9 @@ interface Transport {
/** Identifier shown in introspection and in `deniedFor` on failure entries. */ /** Identifier shown in introspection and in `deniedFor` on failure entries. */
name?: string; name?: string;
} }
type LevelsConfig = Partial<
Record<LogLevel, { enabled: boolean; filter?: (entry: LogEntry) => 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. filter, for fine-grained per-sink routing.
- `name` appears in introspection and in `deniedFor` on failure entries. - `name` appears in introspection and in `deniedFor` on failure entries.
- Sync throws and promise rejections both route to the failure handler. - Sync throws and promise rejections both route to the failure handler.
@ -442,7 +429,7 @@ interface ConsoleTransportOptions {
timestamp?: boolean; // @default true timestamp?: boolean; // @default true
prefix?: boolean; // @default true prefix?: boolean; // @default true
source?: boolean; // @default true — shows "(file.ts:42)" source?: boolean; // @default true — shows "(file.ts:42)"
levels?: LevelsConfig; level?: LogLevel; // threshold cutoff
filter?: (entry: LogEntry) => boolean; // e.g. drop noisy vitals 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, Browser/Node console with the appropriate method per level (TRACE → debug,
FATAL → error). FATAL → error).
### `httpTransport({ url, headers?, levels?, filter? })` ### `httpTransport({ url, headers?, level?, filter? })`
Fire-and-forget POST to an arbitrary endpoint. Defaults to Fire-and-forget POST to an arbitrary endpoint. Defaults to `LogLevel.ERROR`
`levelsAtLeast(LogLevel.ERROR)` to stay quiet by default. No retry built in; to stay quiet by default. No retry built in; batching uses `writeBatch` when
batching uses `writeBatch` when `buffer > 0`. `buffer > 0`.
### `callbackTransport(fn, options?)` ### `callbackTransport(fn, options?)`
Factory that builds a `Transport` out of a callback. Structurally equivalent Factory that builds a `Transport` out of a callback. Structurally equivalent
to `logger.subscribe(fn)` but lets you pre-register it in 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' 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`, Level mapping: TRACE/DEBUG → `debug`, INFO → `info`, WARN → `warn`,
@ -524,7 +511,7 @@ import { Logtail } from '@logtail/browser';
import { logtailTransport } from '$logger/adapters/logtail'; import { logtailTransport } from '$logger/adapters/logtail';
const lt = new Logtail('<source-token>'); const lt = new Logtail('<source-token>');
logger.addTransport(logtailTransport(lt, { levels: levelsAtLeast(LogLevel.INFO) })); logger.addTransport(logtailTransport(lt, { level: LogLevel.INFO }));
``` ```
### Grafana Loki — `lokiTransport(options)` ### Grafana Loki — `lokiTransport(options)`

@ -6,7 +6,7 @@
*/ */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 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 { sentryTransport, type SentryLike, type SentryScopeLike } from './sentry.ts';
import { datadogTransport, type DatadogLoggerLike } from './datadog.ts'; import { datadogTransport, type DatadogLoggerLike } from './datadog.ts';
import { logtailTransport, type LogtailLike } from './logtail.ts'; import { logtailTransport, type LogtailLike } from './logtail.ts';
@ -173,7 +173,7 @@ describe('datadogTransport', () => {
it('maps TRACE/DEBUG to debug, FATAL/ERROR to error', () => { it('maps TRACE/DEBUG to debug, FATAL/ERROR to error', () => {
const { dd, calls } = mockDatadog(); const { dd, calls } = mockDatadog();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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.trace('t', 'a');
log.debug('t', 'b'); log.debug('t', 'b');
@ -188,7 +188,7 @@ describe('datadogTransport', () => {
it('preserves original level in messageContext.logLevel', () => { it('preserves original level in messageContext.logLevel', () => {
const { dd, calls } = mockDatadog(); const { dd, calls } = mockDatadog();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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'); log.fatal('t', 'boom');
const ctx = calls[0].context as Record<string, unknown>; const ctx = calls[0].context as Record<string, unknown>;
expect(ctx.logLevel).toBe('FATAL'); expect(ctx.logLevel).toBe('FATAL');
@ -197,7 +197,7 @@ describe('datadogTransport', () => {
it('uses reserved-safe names (logTags, sourceLocation, durationMs)', () => { it('uses reserved-safe names (logTags, sourceLocation, durationMs)', () => {
const { dd, calls } = mockDatadog(); const { dd, calls } = mockDatadog();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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', { log.info('t', 'hi', {
tags: ['x'], tags: ['x'],
traceId: 'r1', traceId: 'r1',
@ -213,7 +213,7 @@ describe('datadogTransport', () => {
it('passes reconstructed Error to dd.error', () => { it('passes reconstructed Error to dd.error', () => {
const { dd, calls } = mockDatadog(); const { dd, calls } = mockDatadog();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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') }); log.error('t', 'fail', { error: new Error('boom') });
expect(calls[0].error).toBeInstanceOf(Error); expect(calls[0].error).toBeInstanceOf(Error);
@ -251,7 +251,7 @@ describe('logtailTransport', () => {
it('maps levels correctly', () => { it('maps levels correctly', () => {
const { lt, calls } = mockLogtail(); const { lt, calls } = mockLogtail();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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.trace('t', 'a');
log.debug('t', 'b'); log.debug('t', 'b');
@ -266,7 +266,7 @@ describe('logtailTransport', () => {
it('includes logLevel, category and error in context', () => { it('includes logLevel, category and error in context', () => {
const { lt, calls } = mockLogtail(); const { lt, calls } = mockLogtail();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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') }); log.error('auth', 'fail', { error: new Error('boom') });
const ctx = calls[0].context as Record<string, unknown>; const ctx = calls[0].context as Record<string, unknown>;
@ -300,7 +300,7 @@ describe('lokiTransport', () => {
lokiTransport({ lokiTransport({
url: 'https://loki.example.com/loki/api/v1/push', url: 'https://loki.example.com/loki/api/v1/push',
labels: { app: 'test' }, labels: { app: 'test' },
levels: levelsAtLeast(LogLevel.TRACE) level: LogLevel.TRACE
}) })
); );
@ -328,7 +328,7 @@ describe('lokiTransport', () => {
url: 'https://loki.example.com/push', url: 'https://loki.example.com/push',
basicAuthUser: 'user', basicAuthUser: 'user',
basicAuthPassword: 'pass', basicAuthPassword: 'pass',
levels: levelsAtLeast(LogLevel.TRACE) level: LogLevel.TRACE
}) })
); );
@ -347,7 +347,7 @@ describe('lokiTransport', () => {
lokiTransport({ lokiTransport({
url: 'https://loki.example.com/push', url: 'https://loki.example.com/push',
tenantId: 'tenant-1', tenantId: 'tenant-1',
levels: levelsAtLeast(LogLevel.TRACE) level: LogLevel.TRACE
}) })
); );
@ -371,7 +371,7 @@ describe('lokiTransport', () => {
} }
}); });
log.addTransport( 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'); log.info('t', 'hi');
@ -580,7 +580,7 @@ describe('otelTransport', () => {
it('maps levels to OTel severityNumber (1/5/9/13/17/21)', () => { it('maps levels to OTel severityNumber (1/5/9/13/17/21)', () => {
const { otelLogger, emitted } = mockOtelLogger(); const { otelLogger, emitted } = mockOtelLogger();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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.trace('t', 'a');
log.debug('t', 'b'); log.debug('t', 'b');
@ -595,7 +595,7 @@ describe('otelTransport', () => {
it('sets severityText to the level name', () => { it('sets severityText to the level name', () => {
const { otelLogger, emitted } = mockOtelLogger(); const { otelLogger, emitted } = mockOtelLogger();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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'); log.info('t', 'hi');
expect(emitted[0].severityText).toBe('INFO'); expect(emitted[0].severityText).toBe('INFO');
}); });
@ -603,7 +603,7 @@ describe('otelTransport', () => {
it('flattens context as log.context.* attributes', () => { it('flattens context as log.context.* attributes', () => {
const { otelLogger, emitted } = mockOtelLogger(); const { otelLogger, emitted } = mockOtelLogger();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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' } }); log.info('auth', 'hi', { context: { userId: 42, region: 'eu' } });
const attrs = emitted[0].attributes ?? {}; const attrs = emitted[0].attributes ?? {};
@ -615,7 +615,7 @@ describe('otelTransport', () => {
it('maps error fields to exception.* attributes', () => { it('maps error fields to exception.* attributes', () => {
const { otelLogger, emitted } = mockOtelLogger(); const { otelLogger, emitted } = mockOtelLogger();
const log = createEngineLogger({ level: LogLevel.TRACE, transports: [] }); 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') }); log.error('t', 'fail', { error: new Error('boom') });
const attrs = emitted[0].attributes ?? {}; const attrs = emitted[0].attributes ?? {};

@ -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 { LogLevel } from '../types.ts';
import { levelsAtLeast } from '../consts.ts';
/** /**
* Duck-typed subset of the `@datadog/browser-logs` SDK used by the transport. * Duck-typed subset of the `@datadog/browser-logs` SDK used by the transport.
@ -19,8 +18,8 @@ export interface DatadogLoggerLike {
} }
export interface DatadogTransportOptions { export interface DatadogTransportOptions {
/** @default levelsAtLeast(LogLevel.INFO) — INFO/WARN/ERROR/FATAL */ /** @default LogLevel.INFO — INFO/WARN/ERROR/FATAL */
levels?: LevelsConfig; level?: LogLevel;
/** @default 'datadog' */ /** @default 'datadog' */
name?: string; name?: string;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
@ -50,7 +49,7 @@ export interface DatadogTransportOptions {
* version: '1.0.0' * version: '1.0.0'
* }); * });
* *
* logger.addTransport(datadogTransport(datadogLogs.logger, { levels: levelsAtLeast(LogLevel.WARN) })); * logger.addTransport(datadogTransport(datadogLogs.logger, { level: LogLevel.WARN }));
*/ */
export function datadogTransport( export function datadogTransport(
dd: DatadogLoggerLike, dd: DatadogLoggerLike,
@ -58,7 +57,7 @@ export function datadogTransport(
): Transport { ): Transport {
return { return {
name: options.name ?? 'datadog', name: options.name ?? 'datadog',
levels: options.levels ?? levelsAtLeast(LogLevel.INFO), level: options.level ?? LogLevel.INFO,
filter: options.filter, filter: options.filter,
write(entry: LogEntry): void { write(entry: LogEntry): void {
// `source`, `duration`, `tags` are Datadog-reserved attributes — we // `source`, `duration`, `tags` are Datadog-reserved attributes — we

@ -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 { LogLevel } from '../types.ts';
import { levelsAtLeast } from '../consts.ts';
/** /**
* Duck-typed subset of the `@logtail/browser` SDK (Better Stack). * Duck-typed subset of the `@logtail/browser` SDK (Better Stack).
@ -18,8 +17,8 @@ export interface LogtailLike {
} }
export interface LogtailTransportOptions { export interface LogtailTransportOptions {
/** @default levelsAtLeast(LogLevel.INFO) */ /** @default LogLevel.INFO */
levels?: LevelsConfig; level?: LogLevel;
/** @default 'logtail' */ /** @default 'logtail' */
name?: string; name?: string;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
@ -43,7 +42,7 @@ export function logtailTransport(
): Transport { ): Transport {
return { return {
name: options.name ?? 'logtail', name: options.name ?? 'logtail',
levels: options.levels ?? levelsAtLeast(LogLevel.INFO), level: options.level ?? LogLevel.INFO,
filter: options.filter, filter: options.filter,
write(entry: LogEntry): void { write(entry: LogEntry): void {
const context: Record<string, unknown> = { const context: Record<string, unknown> = {

@ -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 { LogLevel } from '../types.ts';
import { LEVEL_LABELS, levelsAtLeast } from '../consts.ts'; import { LEVEL_LABELS } from '../consts.ts';
import { LoggerLokiPushFailedError } from '../errors.ts'; import { LoggerLokiPushFailedError } from '../errors.ts';
export interface LokiTransportOptions { export interface LokiTransportOptions {
@ -19,8 +19,8 @@ export interface LokiTransportOptions {
/** Basic auth user. Equivalent to `Authorization: Basic base64(user:pass)`. */ /** Basic auth user. Equivalent to `Authorization: Basic base64(user:pass)`. */
basicAuthUser?: string; basicAuthUser?: string;
basicAuthPassword?: string; basicAuthPassword?: string;
/** @default levelsAtLeast(LogLevel.INFO) */ /** @default LogLevel.INFO */
levels?: LevelsConfig; level?: LogLevel;
/** @default 'loki' */ /** @default 'loki' */
name?: string; name?: string;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
@ -101,7 +101,7 @@ export function lokiTransport(options: LokiTransportOptions): Transport {
return { return {
name: options.name ?? 'loki', name: options.name ?? 'loki',
levels: options.levels ?? levelsAtLeast(LogLevel.INFO), level: options.level ?? LogLevel.INFO,
filter: options.filter, filter: options.filter,
async write(entry: LogEntry): Promise<void> { async write(entry: LogEntry): Promise<void> {
const streamLabels: Record<string, string> = { const streamLabels: Record<string, string> = {

@ -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 { LogLevel } from '../types.ts';
import { levelsAtLeast } from '../consts.ts';
/** /**
* Duck-typed subset of the OpenTelemetry Logs API. The caller injects an * Duck-typed subset of the OpenTelemetry Logs API. The caller injects an
@ -30,8 +29,8 @@ export interface OtelLogRecord {
} }
export interface OtelTransportOptions { export interface OtelTransportOptions {
/** @default levelsAtLeast(LogLevel.INFO) */ /** @default LogLevel.INFO */
levels?: LevelsConfig; level?: LogLevel;
/** @default 'otel' */ /** @default 'otel' */
name?: string; name?: string;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
@ -58,7 +57,7 @@ export function otelTransport(
): Transport { ): Transport {
return { return {
name: options.name ?? 'otel', name: options.name ?? 'otel',
levels: options.levels ?? levelsAtLeast(LogLevel.INFO), level: options.level ?? LogLevel.INFO,
filter: options.filter, filter: options.filter,
write(entry: LogEntry): void { write(entry: LogEntry): void {
const attributes: Record<string, unknown> = { const attributes: Record<string, unknown> = {

@ -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 { LogLevel } from '../types.ts';
import { levelsAtLeast } from '../consts.ts';
/** /**
* Duck-typed subset of the Sentry SDK used by the transport. The `engine_logger` * Duck-typed subset of the Sentry SDK used by the transport. The `engine_logger`
@ -43,11 +42,11 @@ export interface SentryTransportOptions {
*/ */
breadcrumbLevel?: LogLevel; breadcrumbLevel?: LogLevel;
/** /**
* Optional per-level acceptance override. When undefined, the transport * Optional threshold-style acceptance override. When undefined, the
* accepts `[breadcrumbLevel, FATAL]` — everything required for its dual * transport accepts `[breadcrumbLevel, FATAL]` — everything required for
* event/breadcrumb routing. Set this to restrict further. * its dual event/breadcrumb routing. Set this to restrict further.
*/ */
levels?: LevelsConfig; level?: LogLevel;
/** Transport name shown in introspection and `deniedFor`. @default 'sentry' */ /** Transport name shown in introspection and `deniedFor`. @default 'sentry' */
name?: string; name?: string;
/** Extra predicate. When it returns `false`, the entry is skipped entirely. */ /** Extra predicate. When it returns `false`, the entry is skipped entirely. */
@ -88,7 +87,7 @@ export function sentryTransport(
name, name,
// Accept everything from breadcrumbLevel upwards; the write() body // Accept everything from breadcrumbLevel upwards; the write() body
// decides whether to emit an event or a breadcrumb. // decides whether to emit an event or a breadcrumb.
levels: options.levels ?? levelsAtLeast(breadcrumbLevel), level: options.level ?? breadcrumbLevel,
filter: options.filter, filter: options.filter,
write(entry: LogEntry): void { write(entry: LogEntry): void {
if (entry.level < breadcrumbLevel) return; if (entry.level < breadcrumbLevel) return;

@ -1,5 +1,4 @@
import { LogLevel } from './types.ts'; import { LogLevel } from './types.ts';
import type { LevelsConfig } from './types.ts';
export const LOGGER_MODULE = 'logger'; export const LOGGER_MODULE = 'logger';
@ -29,35 +28,6 @@ export const ALL_LEVELS: readonly LogLevel[] = [
LogLevel.FATAL 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 * 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. * external adapters (e.g. Loki) that ship the level as a string label.

@ -11,8 +11,8 @@ import type {
LogSource, LogSource,
LogError LogError
} from './types.ts'; } from './types.ts';
import { LogLevel, type LevelsConfig } from './types.ts'; import { LogLevel } from './types.ts';
import { ALL_LEVELS, ENGINE_NAME } from './consts.ts'; import { ENGINE_NAME } from './consts.ts';
import { isDev as DEV } from '$libs/env'; import { isDev as DEV } from '$libs/env';
import { consoleTransport } from './transports.ts'; import { consoleTransport } from './transports.ts';
import { captureSource, extractError } from './source.ts'; import { captureSource, extractError } from './source.ts';
@ -47,7 +47,7 @@ function generateId(): string {
* - `child(ctx)` — child logger with extended context, shares history/transports. * - `child(ctx)` — child logger with extended context, shares history/transports.
* - `time(label)` / `timeEnd(label)` — duration measurement. * - `time(label)` / `timeEnd(label)` — duration measurement.
* - `addTransport(t)` / `subscribe(fn)` — dynamic bus (Sentry, metrics, etc.). * - `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. * - Lazy messages via thunks — not evaluated if filtered by global level.
* *
* @example * @example
@ -60,7 +60,7 @@ function generateId(): string {
* // Sentry as a dynamic transport * // Sentry as a dynamic transport
* const detachSentry = log.addTransport({ * const detachSentry = log.addTransport({
* name: 'sentry', * name: 'sentry',
* levels: levelsAtLeast(LogLevel.ERROR), * level: LogLevel.ERROR,
* write: (entry) => { * write: (entry) => {
* if (entry.error) Sentry.captureException(entry.error); * if (entry.error) Sentry.captureException(entry.error);
* else Sentry.captureMessage(entry.message); * else Sentry.captureMessage(entry.message);
@ -84,19 +84,10 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger {
const buffers = new WeakMap<Transport, BufferedLogEntry[]>(); const buffers = new WeakMap<Transport, BufferedLogEntry[]>();
const flushTimers = new WeakMap<Transport, ReturnType<typeof setTimeout>>(); const flushTimers = new WeakMap<Transport, ReturnType<typeof setTimeout>>();
// Mutable state shared between root logger and its children. The // Mutable state shared between root logger and its children. Entries
// engine gates entries by per-level enablement (`enabledLevels`), the // below `level` are dropped before they reach history or transports.
// 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 = { const state = {
level: initialLevel, level: options.level ?? LogLevel.WARN,
enabledLevels: initialEnabled,
maxLogs: options.maxLogs ?? 1000, maxLogs: options.maxLogs ?? 1000,
entries: [] as LogEntry[], entries: [] as LogEntry[],
transports: [...(options.transports ?? [consoleTransport()])] transports: [...(options.transports ?? [consoleTransport()])]
@ -132,36 +123,11 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger {
interface LoggerState { interface LoggerState {
level: LogLevel; level: LogLevel;
enabledLevels: Set<LogLevel>;
maxLogs: number; maxLogs: number;
entries: LogEntry[]; entries: LogEntry[];
transports: Transport[]; 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 { interface DispatchContext {
deniedFor: Set<string>; deniedFor: Set<string>;
} }
@ -265,7 +231,7 @@ function buildLogger(
skipFrames: number skipFrames: number
): void { ): void {
// Global level check first: if dropped, skip thunk evaluation and source capture. // 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 resolved = resolveMessage(message);
const source: LogSource | undefined = const source: LogSource | undefined =
@ -331,19 +297,13 @@ function buildLogger(
} }
/** /**
* Decide whether the transport accepts an entry at its level, using the * Decide whether the transport accepts an entry at its level. When
* per-level `levels` config: * `transport.level` is undefined the entry passes; otherwise the entry
* - no `levels` set → all levels pass * is dropped if `entry.level < transport.level`.
* - level missing or `enabled: false` → drop
* - `enabled: true` → apply the optional per-level filter
*/ */
function transportAcceptsLevel(transport: Transport, entry: LogEntry): boolean { function transportAcceptsLevel(transport: Transport, entry: LogEntry): boolean {
const cfg = transport.levels; if (transport.level === undefined) return true;
if (!cfg) return true; return entry.level >= transport.level;
const entryCfg = cfg[entry.level];
if (!entryCfg || !entryCfg.enabled) return false;
if (entryCfg.filter && !entryCfg.filter(entry)) return false;
return true;
} }
function writeOne(transport: Transport, entry: LogEntry, context: DispatchContext): void { function writeOne(transport: Transport, entry: LogEntry, context: DispatchContext): void {
@ -566,7 +526,6 @@ function buildLogger(
setLevel(l: LogLevel): void { setLevel(l: LogLevel): void {
state.level = l; state.level = l;
state.enabledLevels = buildEnabledLevelsFromThreshold(l);
}, },
setMaxLogs(max: number): void { setMaxLogs(max: number): void {

@ -7,8 +7,6 @@
export { export {
ENGINE_NAME, ENGINE_NAME,
ALL_LEVELS, ALL_LEVELS,
levelsAtLeast,
allLevels,
LEVEL_LABELS, LEVEL_LABELS,
LEVEL_CONSOLE_METHOD LEVEL_CONSOLE_METHOD
} from './consts.ts'; } from './consts.ts';
@ -29,7 +27,6 @@ export type {
LogInput, LogInput,
LogEntry, LogEntry,
LogFilters, LogFilters,
LevelsConfig,
Transport, Transport,
SubscriberFn, SubscriberFn,
ConsoleTransportOptions, ConsoleTransportOptions,

@ -29,7 +29,7 @@ import {
type Transport type Transport
} from '../index.ts'; } from '../index.ts';
import { extractError } from '../source.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[] } { function capture(): { transport: Transport; entries: LogEntry[] } {
const entries: LogEntry[] = []; const entries: LogEntry[] = [];
@ -91,45 +91,6 @@ describe('createEngineLogger — level methods', () => {
expect(entries.map((e) => e.message)).toEqual(['one', 'two']); 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', () => { describe('createEngineLogger — lazy messages', () => {
@ -468,13 +429,13 @@ describe('createEngineLogger — dynamic transports', () => {
}); });
describe('createEngineLogger — per-transport levels and filter', () => { 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 entries: LogEntry[] = [];
const log = createEngineLogger({ level: LogLevel.TRACE }); const log = createEngineLogger({ level: LogLevel.TRACE });
log.removeAllTransports(); log.removeAllTransports();
log.addTransport({ log.addTransport({
name: 't', name: 't',
levels: levelsAtLeast(LogLevel.WARN), level: LogLevel.WARN,
write: (e) => { write: (e) => {
entries.push(e); entries.push(e);
} }
@ -650,9 +611,9 @@ describe('callbackTransport', () => {
expect(hits).toHaveLength(1); expect(hits).toHaveLength(1);
}); });
it('respects levels option', () => { it('respects level option', () => {
const hits: LogEntry[] = []; 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({ const log = createEngineLogger({
level: LogLevel.TRACE, level: LogLevel.TRACE,
transports: [cb] transports: [cb]

@ -6,7 +6,7 @@ import type {
} from './types.ts'; } from './types.ts';
import { LogLevel } 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'; import { LoggerHttpTransportPushFailedError } from './errors.ts';
// ============================================================================ // ============================================================================
@ -22,20 +22,20 @@ import { LoggerHttpTransportPushFailedError } from './errors.ts';
* @example * @example
* consoleTransport() * consoleTransport()
* consoleTransport({ timestamp: false, prefix: false }) * consoleTransport({ timestamp: false, prefix: false })
* consoleTransport({ levels: levelsAtLeast(LogLevel.INFO) }) * consoleTransport({ level: LogLevel.INFO })
*/ */
export function consoleTransport(options: ConsoleTransportOptions = {}): Transport { export function consoleTransport(options: ConsoleTransportOptions = {}): Transport {
const { const {
timestamp: showTimestamp = true, timestamp: showTimestamp = true,
prefix: showPrefix = true, prefix: showPrefix = true,
source: showSource = true, source: showSource = true,
levels, level,
filter filter
} = options; } = options;
return { return {
name: 'console', name: 'console',
levels, level,
filter, filter,
write(entry: LogEntry): void { write(entry: LogEntry): void {
const args: unknown[] = []; const args: unknown[] = [];
@ -111,7 +111,7 @@ export function httpTransport(options: HttpTransportOptions): Transport {
return { return {
name: 'http', name: 'http',
levels: options.levels ?? levelsAtLeast(LogLevel.ERROR), level: options.level ?? LogLevel.ERROR,
filter: options.filter, filter: options.filter,
async write(entry: LogEntry): Promise<void> { async write(entry: LogEntry): Promise<void> {
await post(serialize(entry)); await post(serialize(entry));
@ -131,7 +131,7 @@ export function httpTransport(options: HttpTransportOptions): Transport {
* Transport that invokes a callback for each reaching entry. Structurally * Transport that invokes a callback for each reaching entry. Structurally
* equivalent to `logger.subscribe(fn)` — it exists as a factory so it can be * equivalent to `logger.subscribe(fn)` — it exists as a factory so it can be
* pre-registered via `LoggerOptions.transports` or composed with * pre-registered via `LoggerOptions.transports` or composed with
* `levels`/`filter`/`name`. * `level`/`filter`/`name`.
* *
* @example * @example
* callbackTransport((entry) => captured.push(entry)) * callbackTransport((entry) => captured.push(entry))
@ -143,20 +143,20 @@ export function httpTransport(options: HttpTransportOptions): Transport {
* if (entry.error) Sentry.captureException(entry.error); * if (entry.error) Sentry.captureException(entry.error);
* else Sentry.captureMessage(entry.message); * else Sentry.captureMessage(entry.message);
* }, * },
* { levels: levelsAtLeast(LogLevel.ERROR), name: 'sentry' } * { level: LogLevel.ERROR, name: 'sentry' }
* ) * )
*/ */
export function callbackTransport( export function callbackTransport(
fn: (entry: LogEntry) => void, fn: (entry: LogEntry) => void,
options: { options: {
levels?: import('./types.ts').LevelsConfig; level?: LogLevel;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
name?: string; name?: string;
} = {} } = {}
): Transport { ): Transport {
return { return {
write: fn, write: fn,
levels: options.levels, level: options.level,
filter: options.filter, filter: options.filter,
name: options.name ?? 'callback' name: options.name ?? 'callback'
}; };

@ -65,10 +65,7 @@ export interface LogEntry {
* `getLogs()` filters. All optional — combined with AND. * `getLogs()` filters. All optional — combined with AND.
* *
* - `level` matches the level exactly. * - `level` matches the level exactly.
* - `fromLevel` matches every level at or above the given threshold (mirrors * - `fromLevel` matches every level at or above the given threshold.
* 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.
*/ */
export interface LogFilters { export interface LogFilters {
level?: LogLevel; level?: LogLevel;
@ -90,37 +87,10 @@ export interface LogFilters {
* Can be registered at logger creation (`options.transports`) or dynamically * Can be registered at logger creation (`options.transports`) or dynamically
* via `logger.addTransport()` / `logger.subscribe()`. * via `logger.addTransport()` / `logger.subscribe()`.
* *
* Each transport decides whether to process each entry via `levels` (per- * Each transport decides whether to process each entry via `level` (threshold
* severity acceptance + per-level filter) and `filter` (global predicate) — * cutoff) and `filter` (arbitrary predicate) — both applied after the logger's
* both applied after the logger's global level filter. * 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<LogLevel, { enabled: boolean; filter?: (entry: LogEntry) => boolean }>
>;
export interface Transport { export interface Transport {
/** /**
* Process a single entry. Sync or async. * Process a single entry. Sync or async.
@ -159,17 +129,16 @@ export interface Transport {
*/ */
flushIntervalMs?: number; flushIntervalMs?: number;
/** /**
* Per-level acceptance config. When undefined, every level passes (subject * Threshold-style per-transport filter. Entries with `entry.level < level`
* only to the logger's global level filter). See `LevelsConfig`. * are skipped by this transport (the engine still records them in history
* * if they passed the global level). When undefined, every entry that
* Use `levelsAtLeast(LogLevel.X)` as a shorthand for the common * reaches the transport is accepted.
* "everything from level X and above" case.
*/ */
levels?: LevelsConfig; level?: LogLevel;
/** /**
* Global arbitrary predicate applied after the per-level filter. If it * Arbitrary predicate applied after the threshold check. If it returns
* returns `false`, the entry is skipped. Useful to drop a category/tag * `false`, the entry is skipped. Useful to drop a category/tag regardless
* regardless of level. * of level.
*/ */
filter?(entry: LogEntry): boolean; filter?(entry: LogEntry): boolean;
/** /**
@ -215,7 +184,8 @@ export interface ConsoleTransportOptions {
prefix?: boolean; prefix?: boolean;
/** Include the `source` in parentheses. @default true */ /** Include the `source` in parentheses. @default true */
source?: boolean; source?: boolean;
levels?: LevelsConfig; /** Threshold-style per-transport filter. */
level?: LogLevel;
/** /**
* Predicate that skips noisy entries (vitals, high-volume debug streams) * Predicate that skips noisy entries (vitals, high-volume debug streams)
* without affecting other transports. Returns `false` to drop the entry * without affecting other transports. Returns `false` to drop the entry
@ -231,8 +201,8 @@ export interface ConsoleTransportOptions {
export interface HttpTransportOptions { export interface HttpTransportOptions {
url: string; url: string;
headers?: Record<string, string>; headers?: Record<string, string>;
/** @default levelsAtLeast(LogLevel.ERROR) */ /** @default LogLevel.ERROR */
levels?: LevelsConfig; level?: LogLevel;
filter?: (entry: LogEntry) => boolean; filter?: (entry: LogEntry) => boolean;
} }
@ -242,22 +212,11 @@ export interface HttpTransportOptions {
export interface LoggerOptions { export interface LoggerOptions {
/** /**
* Threshold-style filter. Equivalent to `levels: levelsAtLeast(level)`. * Threshold-style global filter. Entries with `entry.level < level` are
* Provided for ergonomics; `levels` takes precedence when both are set. * dropped before they reach history or transports.
* @default LogLevel.WARN * @default LogLevel.WARN
*/ */
level?: LogLevel; 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 */ /** @default 1000 */
maxLogs?: number; maxLogs?: number;
/** Transports registered at creation time. More can be added dynamically. */ /** Transports registered at creation time. More can be added dynamically. */

@ -3,14 +3,11 @@ import type {
CatalogDiagnosticsOptions, CatalogDiagnosticsOptions,
DiagnosticCatalogEntry, DiagnosticCatalogEntry,
DiagnosticEvent, DiagnosticEvent,
DiagnosticEventConfig,
DiagnosticLogDescriptor, DiagnosticLogDescriptor,
DiagnosticLogEntry,
Diagnostics, Diagnostics,
LoggerDiagnosticsOptions, LoggerDiagnosticsOptions,
Logger, Logger,
LogInput, LogInput
LevelConfig
} from './types.ts'; } from './types.ts';
const DIAGNOSTIC_LOGGER_METHODS: Readonly< const DIAGNOSTIC_LOGGER_METHODS: Readonly<
@ -75,17 +72,6 @@ function resolveCatalogEntry<TEvent extends DiagnosticEvent>(
return entry; return entry;
} }
function logLevelEnabled(
levels: LevelConfig<DiagnosticLogEntry> | 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<TEvent extends DiagnosticEvent>( function resolveDiagnosticOverride<TEvent extends DiagnosticEvent>(
options: CatalogDiagnosticsOptions<TEvent>, options: CatalogDiagnosticsOptions<TEvent>,
event: TEvent event: TEvent
@ -149,15 +135,7 @@ export function createCatalogDiagnostics<TEvent extends DiagnosticEvent>(
const level = override?.level ?? descriptor.level; const level = override?.level ?? descriptor.level;
const category = const category =
descriptor.category ?? event.scope ?? options.defaultCategory ?? event.artifact; descriptor.category ?? event.scope ?? options.defaultCategory ?? event.artifact;
const diagnosticEntry: DiagnosticLogEntry = { if (options.level !== undefined && level < options.level) return undefined;
level,
category,
message: descriptor.message,
tags: descriptor.tags
};
if (!logLevelEnabled(options.levels, level, diagnosticEntry)) {
return undefined;
}
return { return {
...descriptor, ...descriptor,
level, level,

@ -10,7 +10,6 @@ export type {
DiagnosticLogEntry, DiagnosticLogEntry,
Diagnostics, Diagnostics,
LoggerDiagnosticsOptions, LoggerDiagnosticsOptions,
LevelConfig,
Logger, Logger,
LogFn, LogFn,
LogInput, LogInput,

@ -8,10 +8,6 @@ export enum LogLevel {
NONE = 999 NONE = 999
} }
export type LevelConfig<TEntry> = Partial<
Record<LogLevel, { enabled: boolean; filter?: (entry: TEntry) => boolean }>
>;
export type LogMessage = string | (() => string); export type LogMessage = string | (() => string);
export interface LogSource { export interface LogSource {
@ -91,6 +87,12 @@ export interface CatalogDiagnosticsOptions<TEvent extends DiagnosticEvent> {
readonly logger?: Logger; readonly logger?: Logger;
readonly catalog: DiagnosticCatalog<TEvent>; readonly catalog: DiagnosticCatalog<TEvent>;
readonly defaultCategory?: string; readonly defaultCategory?: string;
readonly levels?: LevelConfig<DiagnosticLogEntry>; /**
* 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<Record<TEvent['type'], DiagnosticEventConfig>>; readonly events?: Partial<Record<TEvent['type'], DiagnosticEventConfig>>;
} }

@ -3533,14 +3533,14 @@ const result = await Sium.validate(ProfileSchema, formValue);`
], ],
quickStart: { quickStart: {
title: 'Create logger', title: 'Create logger',
code: `import { createEngineLogger, consoleTransport, levelsAtLeast, LogLevel } from '$logger'; code: `import { createEngineLogger, consoleTransport, LogLevel } from '$logger';
import { sentryTransport } from '$logger/adapters/sentry'; import { sentryTransport } from '$logger/adapters/sentry';
const Logger = createEngineLogger({ const Logger = createEngineLogger({
level: LogLevel.INFO, level: LogLevel.INFO,
transports: [ transports: [
consoleTransport(), consoleTransport(),
sentryTransport(Sentry, { levels: levelsAtLeast(LogLevel.ERROR) }) sentryTransport(Sentry, { level: LogLevel.ERROR })
], ],
globalContext: { appVersion: '1.0.0' } globalContext: { appVersion: '1.0.0' }
}); });

@ -4,8 +4,7 @@
LogLevel, LogLevel,
callbackTransport, callbackTransport,
consoleTransport, consoleTransport,
createEngineLogger, createEngineLogger
levelsAtLeast
} from '$logger'; } from '$logger';
import { sentryTransport } from '$logger/adapters/sentry'; import { sentryTransport } from '$logger/adapters/sentry';
import { registerWebVitals } from '$logger/vitals'; import { registerWebVitals } from '$logger/vitals';
@ -32,7 +31,7 @@
// Filter vitals out of the console to keep it readable — they // Filter vitals out of the console to keep it readable — they
// still flow to Sentry/Datadog/Loki/Capture. // still flow to Sentry/Datadog/Loki/Capture.
consoleTransport({ consoleTransport({
levels: levelsAtLeast(LogLevel.DEBUG), level: LogLevel.DEBUG,
filter: (e) => !e.tags?.includes('vitals') filter: (e) => !e.tags?.includes('vitals')
}), }),
captureTransport captureTransport
@ -127,7 +126,7 @@
function attachFakeSentry(): void { function attachFakeSentry(): void {
const fakeSentry: Transport = { const fakeSentry: Transport = {
name: 'fake-sentry', name: 'fake-sentry',
levels: levelsAtLeast(LogLevel.ERROR), level: LogLevel.ERROR,
write(entry) { write(entry) {
console.log('[fake-sentry] captureException/Message', { console.log('[fake-sentry] captureException/Message', {
message: entry.message, message: entry.message,
@ -496,8 +495,8 @@
{#each activeTransports as t, i (i)} {#each activeTransports as t, i (i)}
<div class="transport"> <div class="transport">
<strong>{t.name ?? 'anonymous'}</strong> <strong>{t.name ?? 'anonymous'}</strong>
{#if t.levels} {#if t.level !== undefined}
<span class="badge">levels: {Object.keys(t.levels).map((k) => levelName(Number(k))).join(', ')}</span> <span class="badge">level: {levelName(t.level)}+</span>
{/if} {/if}
{#if t.filter} {#if t.filter}
<span class="badge">filter</span> <span class="badge">filter</span>

Loading…
Cancel
Save

Powered by TurnKey Linux.