You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/logger/README.md

773 lines
26 KiB

# logger
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
Professional structured logger. **Zero external dependencies.** Decoupled from
i18n. Dynamic transports with per-sink filtering. Async-aware dispatch.
Failure routing with `deniedFor`. Built-in adapters for Sentry, Datadog,
Logtail (Better Stack), Grafana Loki and OpenTelemetry.
---
## Features
- **6 severity levels** aligned with pino / Log4j / OpenTelemetry / Sentry:
`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`.
- **Structured entries** — category, context, tags, traceId, error, durationMs,
source (auto-captured from stack in DEV).
- **Lazy messages** — pass a thunk, evaluated only if the entry passes the
global level filter.
- **globalContext** merged into every entry (appVersion, env, sessionId, ...).
- **Web Vitals integration** — `registerWebVitals()` turns LCP/INP/CLS/FCP/TTFB
into structured log entries; every transport automatically becomes a RUM sink.
- **`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** — `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
synthetic ERROR entry is dispatched to the remaining transports with
`deniedFor: [failedName]`. Guarantees no cascade loop.
- **Built-in adapters** — `sentryTransport`, `datadogTransport`,
`logtailTransport`, `lokiTransport`, `otelTransport`. All accept the SDK as
an injected parameter so `logger` stays dep-free.
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
---
## Architecture
```
logger/
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
├── index.ts Barrel exports
├── types.ts LogLevel, LogEntry, LogInput, Transport, EngineLogger, ...
├── engine-logger.ts Factory createEngineLogger()
├── transports.ts consoleTransport, httpTransport, callbackTransport
├── source.ts captureSource(), extractError()
├── consts.ts ENGINE_NAME (`engine_logger`), LEVEL_LABELS, LEVEL_CONSOLE_METHOD,
│ ALL_LEVELS
├── vitals.ts registerWebVitals(logger, webVitalsSDK, options?)
├── adapters/
│ ├── sentry.ts sentryTransport(sentrySDK, options)
│ ├── datadog.ts datadogTransport(ddLogger, options)
│ ├── logtail.ts logtailTransport(logtailInstance, options)
│ ├── loki.ts lokiTransport({ url, labels, ... })
│ └── otel.ts otelTransport(otelLogger, options)
└── test/
└── logger.test.ts Unit tests (vitest, node env)
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
```
## Alias
Configured in `svelte.config.js`:
```js
alias: { $logger: 'src/arts/logger' }
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
```
Imports:
```ts
import { createEngineLogger, LogLevel, consoleTransport } from '$logger';
import { sentryTransport } from '$logger/adapters/sentry';
import { datadogTransport } from '$logger/adapters/datadog';
import { logtailTransport } from '$logger/adapters/logtail';
import { lokiTransport } from '$logger/adapters/loki';
import { otelTransport } from '$logger/adapters/otel';
import { registerWebVitals } from '$logger/vitals';
```
---
## Quick start
```ts
import { createEngineLogger, LogLevel, consoleTransport } from '$logger';
const logger = createEngineLogger({
level: LogLevel.DEBUG,
globalContext: { appVersion: '1.2.3', env: 'prod' },
transports: [consoleTransport()]
});
logger.info('auth', 'Login succeeded', {
context: { userId: 42 },
tags: ['auth', 'security'],
traceId: 'req-abc-123'
});
try { ... } catch (err) {
logger.error('db', 'Query failed', { error: err, traceId: 'req-abc-123' });
}
// Duration measurement
logger.time('fetch-user');
await fetchUser();
logger.timeEnd('fetch-user', 'perf'); // emits INFO with durationMs and tag 'timer'
// Lazy message — only evaluated if the entry passes the level filter
logger.debug('sql', () => `Query: ${expensiveFormat(query)}`);
```
---
## Log levels
```ts
enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5,
NONE = 999 // disables all logs
}
```
| Level | Intended use |
| ----- | --------------------------------------------------------------------- |
| TRACE | Very verbose tracing (spans, frame-by-frame) |
| DEBUG | Development diagnostics |
| INFO | Normal flow events worth noting |
| WARN | Unexpected but non-interrupting situations |
| ERROR | Recoverable errors that need attention |
| FATAL | Unrecoverable failures (route to immediate paging) |
### Level → string lookups (for custom transports)
`logger/consts` exports two readonly tables that adapter authors can reuse
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
instead of re-rolling another `switch (level)`:
```ts
import { LEVEL_LABELS, LEVEL_CONSOLE_METHOD, LogLevel } from '$logger';
LEVEL_LABELS[LogLevel.WARN]; // 'warn'
LEVEL_CONSOLE_METHOD[LogLevel.FATAL]; // 'error' (FATAL collapses to console.error)
```
- `LEVEL_LABELS` — canonical string per level (`trace | debug | info | warn |
error | fatal`). Used by `vitals` for category routing and by the bundled
Loki adapter.
- `LEVEL_CONSOLE_METHOD` — `console` method name per level (`debug | info |
warn | error`). TRACE collapses to `debug`, FATAL to `error` because the
console exposes neither natively.
Both omit `LogLevel.NONE` (which is a routing decision, not an emitted level).
---
## API
### `createEngineLogger(options)`
```ts
interface LoggerOptions {
level?: LogLevel; // @default LogLevel.WARN
maxLogs?: number; // @default 1000
transports?: Transport[]; // @default [consoleTransport()]
globalContext?: Record<string, unknown>;
captureSource?: boolean; // @default true in DEV, false in PROD
}
```
### Level methods
```ts
logger.trace(category, message, input?)
logger.debug(category, message, input?)
logger.info (category, message, input?)
logger.warn (category, message, input?)
logger.error(category, message, input?)
logger.fatal(category, message, input?)
```
Where:
```ts
type LogMessage = string | (() => string); // lazy thunks supported
interface LogInput {
context?: Record<string, unknown>; // merged with globalContext
error?: unknown; // extracted to LogError { name, message, stack, cause }
tags?: string[]; // finer-grained filtering than category
traceId?: string; // correlate across async flows
durationMs?: number; // auto-set by timeEnd()
source?: LogSource; // manual override of auto-captured file:line
}
```
### History
```ts
logger.getLogs(filters?) // defensive copy, filtered by level/fromLevel/category/tag/traceId/since
logger.clear()
logger.serialize() // JSON string (circular-safe)
```
### Runtime controls
```ts
logger.setLevel(LogLevel.WARN)
logger.setMaxLogs(500)
logger.setGlobalContext({ appVersion: '1.3.0' })
```
### Lifecycle
```ts
logger.flush(); // flush every buffered transport synchronously
logger.dispose(); // detach beforeunload listener, flush, drop transports, clear history
```
`dispose()` is idempotent. Children created via `child()` do not own the
lifecycle — calling `dispose()` on a child is a no-op (DEV warning).
### Dynamic transports
```ts
const remove = logger.addTransport(transport); // returns a detach fn
const unsub = logger.subscribe((entry) => ...); // sugar over addTransport
logger.removeAllTransports();
logger.transports(); // read-only snapshot
```
### Child loggers
```ts
const reqLog = logger.child({ requestId: 'req-1', userId: 42 });
reqLog.info('auth', 'authenticated'); // context merges parent + child
```
Children **share** history, transports and level with the parent. They only
extend `globalContext`.
### Timers
```ts
logger.time('fetch');
await doWork();
logger.timeEnd('fetch', 'perf', 'operation done'); // emits INFO with durationMs
```
---
## Framework Logger Contract
El contrato minimo para cualquier modulo vive en `$libs/logger`:
```ts
import type { Logger, LogInput, LogMessage } from '$libs/logger';
type LogFn = (category: string, message: LogMessage, input?: LogInput) => void;
export interface Logger {
trace: LogFn;
debug: LogFn;
info: LogFn;
warn: LogFn;
error: LogFn;
fatal: LogFn;
}
```
Los artefactos no deben declarar mini-loggers locales ni aliases reducidos por
modulo. Si un modulo necesita loggear libremente, recibe
`logger?: Logger` y llama al nivel que corresponda.
`EngineLogger` vive en `arts/logger` y extiende ese contrato con capacidades de
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
runtime: transports, history, children, timers, flush y dispose. El resto del
framework no necesita conocer esas capacidades para emitir logs.
## Framework Diagnostics
`Diagnostics` es una capa opcional encima de `Logger`, pensada para eventos
internos repetibles del framework: listener que lanza, reconnect agotado,
validacion fallida, cache invalidada, etc.
```ts
import { createCatalogDiagnostics, LogLevel, type Diagnostics } from '$libs/logger';
import type { Logger } from '$libs/logger';
type ConnEvent =
| { artifact: 'connection'; type: 'reconnect_exhausted'; meta: { attempts: number } }
| { artifact: 'connection'; type: 'transport_error'; meta: { error: unknown } };
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
function createConnDiagnostics(logger?: Logger): Diagnostics<ConnEvent> {
return createCatalogDiagnostics({
logger,
defaultCategory: 'connection',
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
catalog: {
reconnect_exhausted: {
level: LogLevel.WARN,
message: 'reconnect attempts exhausted'
},
transport_error: {
level: LogLevel.ERROR,
message: 'transport error'
}
}
});
}
```
La idea no es reemplazar `logger.info(...)` ni obligar a declarar cada log como
evento. La regla es:
- Usa `logger.info(...)`, `logger.warn(...)`, etc. para logs libres de negocio o
de modulo.
- Usa `Diagnostics.emit(...)` para eventos internos catalogados que queremos
mantener homogeneos, con categoria/mensaje/nivel centralizados.
- El diagnostic conserva `diagnostics.logger`, asi que sigue siendo un logger
normal por debajo.
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,
level: LogLevel.WARN,
events: {
transport_error: { level: LogLevel.ERROR },
reconnect_exhausted: false
}
});
```
El `EngineLogger` sigue siendo quien decide finalmente si la entrada se emite,
se guarda, pasa filtros de transporte o se manda a Sentry/Datadog/Loki/etc.
`Diagnostics` solo traduce un evento interno a una llamada normal del logger.
---
## Transport interface
```ts
interface Transport {
write(entry: LogEntry): void | Promise<void>;
writeBatch?(entries: LogEntry[]): void | Promise<void>;
/** 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;
/** Periodic flush interval (ms) when `buffer > 0`. @default 0 */
flushIntervalMs?: number;
/** Identifier shown in introspection and in `deniedFor` on failure entries. */
name?: string;
}
```
- `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.
---
## Failure routing (`deniedFor`)
If `transport.write()` throws or rejects:
1. A synthetic `ERROR` entry is created with:
- `category: 'engine_logger'`
- `message: 'Transport "X" failed: ...'`
- `error`: structured `LogError` from the thrown value
- `tags: ['engine_logger', 'transport-failure']`
- `deniedFor: ['X']`
2. The failure entry is dispatched to every **other** transport, skipping the
failed one by reference.
3. If a second transport throws while handling the failure entry, the
secondary failure is **swallowed** (console-only) — this guard guarantees
no cascade loop.
This is why you can attach Sentry (or any other transport) safely: if Sentry
is down, the failure gets logged to console/Datadog/Loki/etc., but never
back to Sentry.
### Throttling failure entries (`failureThrottleMs`)
Without throttling, a single down transport can multiply your log volume:
when Sentry is unavailable, every `log.info(...)` produces a failure entry
that flows to every other sink. With 1000 logs that is 1000 extra Datadog
entries.
Set a per-transport throttle to collapse the cascade into one entry per
window:
```ts
logger.addTransport(sentryTransport(Sentry, {
name: 'sentry',
failureThrottleMs: 60_000 // one failure entry per minute, max
}));
```
Behavior:
- **First failure** — dispatch normally; open a `failureThrottleMs` window.
- **Failures inside the window** — suppressed (counter incremented, no
dispatch). The actual `transport.write()` keeps being invoked on
subsequent application logs so a recovery is detected immediately.
- **First failure after the window closes** — dispatch with
`(+N suppressed in last Xms)` appended to the message and
`{ suppressedCount: N, throttleMs: X }` merged into the entry context, so
downstream sinks can chart the suppression rate.
`failureThrottleMs: 0` (default) preserves the original "every failure
dispatches" behavior. Each transport throttles independently. Children
created via `child()` share the throttle state with the root.
---
## Built-in transports
### `consoleTransport(options?)`
```ts
interface ConsoleTransportOptions {
timestamp?: boolean; // @default true
prefix?: boolean; // @default true
source?: boolean; // @default true — shows "(file.ts:42)"
level?: LogLevel; // threshold cutoff
filter?: (entry: LogEntry) => boolean; // e.g. drop noisy vitals
}
```
Browser/Node console with the appropriate method per level (TRACE → debug,
FATAL → error).
### `httpTransport({ url, headers?, level?, filter? })`
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 `level`, `filter`, `name`.
---
## Adapters
All adapters follow the same pattern: **receive the third-party SDK as an
injected parameter**. `logger` itself never imports `@sentry/*`, `@datadog/*`,
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
etc. — callers install those packages and pass the already-initialized SDK.
This keeps `logger` zero-dep and makes tests trivial (mock the SDK with a plain
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
object).
### Sentry — `sentryTransport(SentrySDK, options?)`
```ts
import * as Sentry from '@sentry/browser';
import { sentryTransport } from '$logger/adapters/sentry';
Sentry.init({ dsn: '<your-dsn>', environment: 'prod' });
logger.addTransport(sentryTransport(Sentry, {
eventLevel: LogLevel.ERROR, // @default ERROR — entries at or above become Sentry events
breadcrumbLevel: LogLevel.INFO // @default INFO — entries in [breadcrumb, event) become breadcrumbs
}));
```
- `ERROR+` → `captureException(entry.error)` if present, otherwise
`captureMessage(entry.message, severity)`.
- `[breadcrumbLevel, eventLevel)` → `addBreadcrumb(...)` attached to the next
captured event.
- `< breadcrumbLevel` → ignored.
- Every event passes through `Sentry.withScope()` to attach tags
(`category`, `traceId`, `tag:x`), contexts (`log.context`, `log.source`,
`log.timing`) without polluting the global scope.
### Datadog — `datadogTransport(ddLogger, options?)`
```ts
import { datadogLogs } from '@datadog/browser-logs';
import { datadogTransport } from '$logger/adapters/datadog';
datadogLogs.init({
clientToken: 'pubXXXXXXXXXX',
site: 'datadoghq.eu',
service: 'my-app',
env: 'prod',
version: '1.0.0'
});
logger.addTransport(datadogTransport(datadogLogs.logger, { level: LogLevel.WARN }));
```
Level mapping: TRACE/DEBUG → `debug`, INFO → `info`, WARN → `warn`,
ERROR/FATAL → `error`. Original level preserved in `messageContext.logLevel`.
Uses reserved-safe names (`logTags`, `sourceLocation`, `durationMs`) to avoid
Datadog's reserved attributes (`source`, `duration`, `tags`).
### Logtail (Better Stack) — `logtailTransport(logtailInstance, options?)`
```ts
import { Logtail } from '@logtail/browser';
import { logtailTransport } from '$logger/adapters/logtail';
const lt = new Logtail('<source-token>');
logger.addTransport(logtailTransport(lt, { level: LogLevel.INFO }));
```
### Grafana Loki — `lokiTransport(options)`
No SDK required — pure HTTP POST to the Loki push endpoint.
```ts
import { lokiTransport } from '$logger/adapters/loki';
logger.addTransport(lokiTransport({
url: 'https://logs-prod-006.grafana.net/loki/api/v1/push',
labels: { app: 'my-app', env: 'prod' }, // low-cardinality only!
basicAuthUser: '123456',
basicAuthPassword: 'eyJhbG...',
tenantId: 'my-tenant' // X-Scope-OrgID header
}));
```
The **level** goes into the stream labels (low cardinality, safe). Everything
else (context, tags, traceId, error) is JSON-encoded in the log line so you
can query with LogQL:
`{app="my-app"} | json | traceId="req-1"`.
### OpenTelemetry — `otelTransport(otelLogger, options?)`
```ts
import { logs } from '@opentelemetry/api-logs';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { otelTransport } from '$logger/adapters/otel';
const provider = new LoggerProvider({ /* ... */ });
logs.setGlobalLoggerProvider(provider);
const otelLogger = logs.getLogger('my-app');
logger.addTransport(otelTransport(otelLogger));
```
Maps `LogLevel` to OTel severityNumber: TRACE=1, DEBUG=5, INFO=9, WARN=13,
ERROR=17, FATAL=21 (base of each 4-number band per the OTel spec).
Structured context, tags, source and error flatten into OTel attributes
using dotted keys (`log.context.userId`, `code.filepath`, `exception.type`,
etc.).
---
## Web Vitals (RUM)
`logger` integrates with Google's [`web-vitals`](https://www.npmjs.com/package/web-vitals)
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
package so every Core Web Vital becomes a structured `LogEntry`. This turns
the logger into a **Real User Monitoring bus**: the same pipeline that ships
errors to Sentry and logs to Loki also ships LCP/INP/CLS/FCP/TTFB samples,
tagged with the route, browser, session, etc. Hot zones and regressions
become queryable per category/tag like any other log.
### Setup
```ts
import * as webVitals from 'web-vitals';
import { registerWebVitals } from '$logger/vitals';
registerWebVitals(logger, webVitals, {
// Promote 'poor' to WARN so dashboards light it up.
levels: { poor: LogLevel.WARN }
});
```
By default each metric emits a single entry with:
- `category: 'vitals'`
- `durationMs: metric.value`
- `tags: ['vitals', <metric>, <rating>]` — e.g. `['vitals', 'lcp', 'poor']`
- `context: { metric, value, rating, delta, metricId, navigationType, route }`
- `level` based on the rating: `good → DEBUG`, `needs-improvement → INFO`, `poor → WARN`
### Options
```ts
interface VitalsOptions {
category?: string; // @default 'vitals'
levels?: Partial<Record<WebVitalRating, LogLevel>>;
metrics?: ('LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB')[];
tags?: string[]; // extra tags added to every entry
reportAllChanges?: boolean; // @default false (only final values)
includeRoute?: boolean; // @default true — adds location.pathname
}
```
### Silencing the console
Web Vitals fire on every page load, so you usually don't want them in the
browser console. Filter them at the `consoleTransport` level; other
transports still receive them unchanged:
```ts
const logger = createEngineLogger({
transports: [
consoleTransport({
filter: (e) => !e.tags?.includes('vitals')
}),
// Loki/Sentry/Datadog still get the vitals entries.
sentryTransport(Sentry),
lokiTransport({ url: '...' })
]
});
```
### Hot-zone analysis patterns
Because vitals entries carry `context.route` and `context.rating`, you can
query directly in your observability backend:
- **LogQL (Loki):**
`{app="my-app",level="warn"} | json | metric="LCP" | rating="poor"` → worst-LCP pages
- **Sentry discover:** group events by `tag.category = vitals` and `tag.tag:lcp`, plot p75 per release
- **Datadog logs:** facet on `metric` and `route`, dashboard p95/p99 per route
### Vitals + Sentry: two delivery paths
Vitals can reach Sentry through **two independent pipelines**, each feeding
a different Sentry product. Knowing which is which avoids noise and gives
you the right dashboard for the question you're asking.
#### Path A — through the logger (what `registerWebVitals` does)
Vitals flow as regular `LogEntry` objects through every transport, including
`sentryTransport`. They end up as **Sentry Issues** (if promoted to ERROR)
or **breadcrumbs** (at INFO/WARN), alongside your normal logs.
| Rating | Default level | Reaches Sentry as … |
| ------------------ | ------------- | ------------------------------------------------------------------ |
| `good` | DEBUG | Dropped (below `breadcrumbLevel: INFO`) |
| `needs-improvement`| INFO | Breadcrumb (attached to the next captured event, not a new Issue) |
| `poor` | WARN | Breadcrumb (still below `eventLevel: ERROR`) |
With the defaults, vitals rarely appear as Sentry **Issues** — **this is by
design**. In production you do not want every page load producing an Issue.
**Best production use of Path A:** lower `breadcrumbLevel` so vitals become
rich context on the next captured error, without flooding the Issues feed:
```ts
logger.addTransport(sentryTransport(Sentry, {
eventLevel: LogLevel.ERROR,
breadcrumbLevel: LogLevel.TRACE // every sample becomes a breadcrumb
}));
```
**Validation trick for dev pages** — temporarily promote every sample to
ERROR so they show up as Issues and you can confirm the pipeline:
```ts
registerWebVitals(logger, webVitals, {
levels: {
good: LogLevel.ERROR,
'needs-improvement': LogLevel.ERROR,
poor: LogLevel.ERROR
}
});
```
#### Path B — through Sentry's native Browser Tracing (recommended for production)
Sentry's own `browserTracingIntegration` auto-captures Web Vitals and emits
them as **Performance transactions**. These land in a dedicated product and
do not pollute the Issues feed.
Where to find them in the Sentry dashboard:
- **Insights → Browser → Web Vitals** — aggregate p75/p95 per route, browser,
device, release
- **Performance → Transactions** — per-pageload detail with full vitals,
resource timing and long tasks
Setup (this is a one-time thing at app init, nothing to do in `logger`):
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
```ts
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: '<your-dsn>',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.1 // 10% of page loads send a transaction
});
```
Vitals now flow automatically — no call to `registerWebVitals` needed for
this path.
#### When to use which
| Use case | Path |
| ----------------------------------------------------------- | ---- |
| Aggregate dashboards (p75 LCP by route, regressions by release) | **B** (Sentry Performance) |
| Correlate vitals with a specific error (debugging triage) | **A** at `breadcrumbLevel: TRACE` (breadcrumbs on error events) |
| Alerts on poor ratings (`poor` LCP → PagerDuty) | **A** with `poor: LogLevel.ERROR` + Sentry Alert rule on the tag |
| Ship vitals to **non-Sentry** backends (Loki, Datadog, OTel) | **A** — the logger fans out to every transport |
| Validate the pipeline during development | **A** with all ratings at ERROR (noisy but visible in Issues) |
The two paths are complementary and can run in parallel. In production the
common setup is **Path B for analytics + Path A as breadcrumbs** — you get
the aggregate view under Performance and contextual vitals on every error,
without a single extra Issue.
---
## Patterns
### Sentry + breadcrumbs + metrics
```ts
// Events in Sentry (ERROR+) and breadcrumbs (INFO/WARN)
logger.addTransport(sentryTransport(Sentry));
// Counter of errors
logger.subscribe((entry) => {
if (entry.level >= LogLevel.ERROR) metrics.errorsPerMinute.inc();
});
```
### Scoped child for a request
```ts
export async function handle(request: Request) {
const reqLog = logger.child({
requestId: crypto.randomUUID(),
path: new URL(request.url).pathname
});
reqLog.info('http', 'incoming request');
try {
return await handler(request, reqLog);
} catch (err) {
reqLog.error('http', 'handler threw', { error: err });
throw err;
}
}
```
### Feature-flagged transport
```ts
if (flags.enableRemoteLogs) {
const detach = logger.addTransport(httpTransport({ url: '...' }));
onFlagDisable(() => detach());
}
```
---
## Tests
```bash
npx vitest run --project server
```
Covers 171 cases: level filtering, lazy messages, globalContext merging,
error extraction, tags/traceId, time/timeEnd, child semantics, getLogs
filters, dynamic add/remove, per-transport levels/filter, sync and async
failure routing with `deniedFor`, cascade guard, serialize, plus adapter
unit tests (Sentry, Datadog, Logtail, Loki, OpenTelemetry) using mocked SDKs.

Powered by TurnKey Linux.