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.
- **`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<void>` 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<void>;
writeBatch?(entries: LogEntry[]): void | Promise<void>;
/** 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<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.
- `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('<source-token>');
logger.addTransport(logtailTransport(lt, { levels: levelsAtLeast(LogLevel.INFO) }));
logger.addTransport(logtailTransport(lt, { level: LogLevel.INFO }));
```
### Grafana Loki — `lokiTransport(options)`

@ -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<string, unknown>;
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<string, unknown>;
@ -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 ?? {};

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

@ -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<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 { 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<void> {
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 { 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<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 { 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;

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

@ -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<Transport, BufferedLogEntry[]>();
const flushTimers = new WeakMap<Transport, ReturnType<typeof setTimeout>>();
// 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<LogLevel>;
maxLogs: number;
entries: LogEntry[];
transports: Transport[];
}
/**
* Convert a threshold `LogLevel` into the equivalent `Set<LogLevel>`
* of enabled levels. A threshold of WARN enables WARN + ERROR + FATAL.
*/
function buildEnabledLevelsFromThreshold(level: LogLevel): Set<LogLevel> {
const result = new Set<LogLevel>();
for (const l of ALL_LEVELS) {
if (l >= level) result.add(l);
}
return result;
}
/**
* Project a `LevelsConfig` (per-level enable map) into the
* `Set<LogLevel>` the engine consults at every log site.
*/
function buildEnabledLevelsFromLevelsConfig(levels: LevelsConfig): Set<LogLevel> {
const result = new Set<LogLevel>();
for (const l of ALL_LEVELS) {
if (levels[l]?.enabled) result.add(l);
}
return result;
}
interface DispatchContext {
deniedFor: Set<string>;
}
@ -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 {

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

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

@ -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<void> {
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'
};

@ -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<LogLevel, { enabled: boolean; filter?: (entry: LogEntry) => 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<string, string>;
/** @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. */

@ -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<TEvent extends DiagnosticEvent>(
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>(
options: CatalogDiagnosticsOptions<TEvent>,
event: TEvent
@ -149,15 +135,7 @@ export function createCatalogDiagnostics<TEvent extends DiagnosticEvent>(
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,

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

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

@ -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' }
});

@ -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)}
<div class="transport">
<strong>{t.name ?? 'anonymous'}</strong>
{#if t.levels}
<span class="badge">levels: {Object.keys(t.levels).map((k) => levelName(Number(k))).join(', ')}</span>
{#if t.level !== undefined}
<span class="badge">level: {levelName(t.level)}+</span>
{/if}
{#if t.filter}
<span class="badge">filter</span>

Loading…
Cancel
Save

Powered by TurnKey Linux.