@ -60,7 +60,9 @@ logger/
Configured in `svelte.config.js` :
```js
alias: { $logger: 'src/arts/logger' }
alias: {
$logger: 'src/arts/logger';
}
```
Imports:
@ -113,24 +115,24 @@ logger.debug('sql', () => `Query: ${expensiveFormat(query)}`);
```ts
enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5,
NONE = 999 // disables all logs
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 | 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)
@ -140,15 +142,15 @@ instead of re-rolling another `switch (level)`:
```ts
import { LEVEL_LABELS, LEVEL_CONSOLE_METHOD, LogLevel } from '$logger';
LEVEL_LABELS[LogLevel.WARN]; // 'warn'
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
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
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).
@ -161,11 +163,11 @@ Both omit `LogLevel.NONE` (which is a routing decision, not an emitted level).
```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?: 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
}
```
@ -183,15 +185,15 @@ logger.fatal(category, message, input?)
Where:
```ts
type LogMessage = string | (() => string); // lazy thunks supported
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
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
}
```
@ -206,16 +208,16 @@ logger.serialize() // JSON string (circular-safe)
### Runtime controls
```ts
logger.setLevel(LogLevel.WARN)
logger.setMaxLogs(500)
logger.setGlobalContext({ appVersion: '1.3.0' })
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
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
@ -245,14 +247,14 @@ extend `globalContext`.
```ts
logger.time('fetch');
await doWork();
logger.timeEnd('fetch', 'perf', 'operation done'); // emits INFO with durationMs
logger.timeEnd('fetch', 'perf', 'operation done'); // emits INFO with durationMs
```
---
## Framework Logger Contract
El contrato minimo para cualquier modulo vive e n `$libs/logger` :
The minimal contract for any module lives i n `$libs/logger` :
```ts
import type { Logger, LogInput, LogMessage } from '$libs/logger';
@ -269,19 +271,19 @@ export interface Logger {
}
```
Los artefactos no deben declarar mini-loggers locales ni aliases reducidos por
modulo. Si un modulo necesita loggear libremente, recib e
`logger?: Logger` y llama al nivel que corresponda .
Artifacts must not declare local mini-loggers or narrowed per-module aliases.
If a module needs to log freely, it receives `logger?: Logger` and calls th e
appropriate level .
`EngineLogger` vive en `arts/logger` y extiende ese contrato con capacidades d e
runtime: transports, history, children, timers, flush y dispose. El resto del
framework no necesita conocer esas capacidades para emitir logs.
`EngineLogger` lives in `arts/logger` and extends that contract with runtim e
capabilities: transports, history, children, timers, flush and dispose. The rest
of the framework does not need to know those capabilities to emit 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.
`Diagnostics` is an optional layer on top of `Logger` , meant for repeatable
internal framework events: a listener that throws, reconnect exhausted, failed
validation, invalidated cache , etc.
```ts
import { createCatalogDiagnostics, LogLevel, type Diagnostics } from '$libs/logger';
@ -309,18 +311,18 @@ function createConnDiagnostics(logger?: Logger): Diagnostics<ConnEvent> {
}
```
La idea no es reemplazar `logger.info(...)` ni obligar a declarar cada log como
evento. La regla e s:
The idea is not to replace `logger.info(...)` nor to force every log to be
declared as an event. The rule i s:
- 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 .
- Use `logger.info(...)` , `logger.warn(...)` , etc. for free-form business or
module l ogs .
- Use `Diagnostics.emit(...)` for catalogued internal events we want to keep
homogeneous, with category/message/level centralized .
- The diagnostic keeps `diagnostics.logger` , so it is still a normal logger
underneath .
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:
Diagnostics accept an optional threshold in the logger's style; if the catalog
descriptor falls below it, the event is dropped before reaching the logger:
```ts
const diagnostics = createCatalogDiagnostics({
@ -334,9 +336,9 @@ const diagnostics = createCatalogDiagnostics({
});
```
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 .
`EngineLogger` remains the one that ultimately decides whether the entry is
emitted, stored, passes transport filters or is sent to Sentry/Datadog/Loki/etc.
`Diagnostics` only translates an internal event into a normal logger call .
---
@ -344,18 +346,18 @@ se guarda, pasa filtros de transporte o se manda a Sentry/Datadog/Loki/etc.
```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;
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;
}
```
@ -397,10 +399,12 @@ 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
}));
logger.addTransport(
sentryTransport(Sentry, {
name: 'sentry',
failureThrottleMs: 60_000 // one failure entry per minute, max
})
);
```
Behavior:
@ -426,11 +430,11 @@ created via `child()` share the throttle state with the root.
```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
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
}
```
@ -467,10 +471,12 @@ 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
}));
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
@ -489,11 +495,11 @@ 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'
clientToken: 'pubXXXXXXXXXX',
site: 'datadoghq.eu',
service: 'my-app',
env: 'prod',
version: '1.0.0'
});
logger.addTransport(datadogTransport(datadogLogs.logger, { level: LogLevel.WARN }));
@ -521,13 +527,15 @@ 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
}));
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
@ -542,7 +550,9 @@ import { logs } from '@opentelemetry/api-logs';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { otelTransport } from '$logger/adapters/otel';
const provider = new LoggerProvider({ /* ... */ });
const provider = new LoggerProvider({
/* ... */
});
logs.setGlobalLoggerProvider(provider);
const otelLogger = logs.getLogger('my-app');
@ -573,8 +583,8 @@ 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 }
// Promote 'poor' to WARN so dashboards light it up.
levels: { poor: LogLevel.WARN }
});
```
@ -590,12 +600,12 @@ By default each metric emits a single entry with:
```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
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
}
```
@ -607,14 +617,14 @@ 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: '...' })
]
transports: [
consoleTransport({
filter: (e) => !e.tags?.includes('vitals')
}),
// Loki/Sentry/Datadog still get the vitals entries.
sentryTransport(Sentry),
lokiTransport({ url: '...' })
]
});
```
@ -640,11 +650,11 @@ 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` ) |
| 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.
@ -653,10 +663,12 @@ design**. In production you do not want every page load producing an Issue.
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
}));
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
@ -664,11 +676,11 @@ 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
}
levels: {
good: LogLevel.ERROR,
'needs-improvement': LogLevel.ERROR,
poor: LogLevel.ERROR
}
});
```
@ -679,6 +691,7 @@ 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,
@ -690,9 +703,9 @@ Setup (this is a one-time thing at app init, nothing to do in `logger`):
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: '< your-dsn > ',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.1 // 10% of page loads send a transaction
dsn: '< your-dsn > ',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.1 // 10% of page loads send a transaction
});
```
@ -701,13 +714,13 @@ 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) |
| 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
@ -726,7 +739,7 @@ logger.addTransport(sentryTransport(Sentry));
// Counter of errors
logger.subscribe((entry) => {
if (entry.level >= LogLevel.ERROR) metrics.errorsPerMinute.inc();
if (entry.level >= LogLevel.ERROR) metrics.errorsPerMinute.inc();
});
```
@ -734,17 +747,17 @@ logger.subscribe((entry) => {
```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;
}
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;
}
}
```
@ -752,8 +765,8 @@ export async function handle(request: Request) {
```ts
if (flags.enableRemoteLogs) {
const detach = logger.addTransport(httpTransport({ url: '...' }));
onFlagDisable(() => detach());
const detach = logger.addTransport(httpTransport({ url: '...' }));
onFlagDisable(() => detach());
}
```