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.
dev a6077dd357
Initial commit: lang + logr libraries with SvelteKit scaffold
6 months ago
..
adapters Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
test Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
README.md Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
consts.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
engine-logger.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
index.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
source.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
transports.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago
types.ts Initial commit: lang + logr libraries with SvelteKit scaffold 6 months ago

README.md

logr

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, ...).
  • 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 — minLevel and filter at the transport level.
  • 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 logr stays dep-free.

Architecture

logr/
├── 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              runtime identifier (`engine_logger`)
├── 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/
    └── logr.test.ts       Unit tests (vitest, node env)

Alias

Configured in svelte.config.js:

alias: { $logr: 'src/arts/logr' }

Imports:

import { createEngineLogger, LogLevel, consoleTransport } from '$logr';
import { sentryTransport } from '$logr/adapters/sentry';
import { datadogTransport } from '$logr/adapters/datadog';
import { logtailTransport } from '$logr/adapters/logtail';
import { lokiTransport } from '$logr/adapters/loki';
import { otelTransport } from '$logr/adapters/otel';

Quick start

import { createEngineLogger, LogLevel, consoleTransport } from '$logr';

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

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)

API

createEngineLogger(options)

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

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:

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

logger.getLogs(filters?)   // defensive copy, filtered by level/minLevel/category/tag/traceId/since
logger.clear()
logger.serialize()         // JSON string (circular-safe)

Runtime controls

logger.setLevel(LogLevel.WARN)
logger.setMaxLogs(500)
logger.setGlobalContext({ appVersion: '1.3.0' })

Dynamic transports

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

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

logger.time('fetch');
await doWork();
logger.timeEnd('fetch', 'perf', 'operation done');  // emits INFO with durationMs

Transport interface

interface Transport {
    write(entry: LogEntry): void | Promise<void>;
    minLevel?: LogLevel;
    filter?(entry: LogEntry): boolean;
    name?: string;
}
  • minLevel 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.


Built-in transports

consoleTransport(options?)

interface ConsoleTransportOptions {
    timestamp?: boolean;   // @default true
    prefix?: boolean;      // @default true
    source?: boolean;      // @default true — shows "(file.ts:42)"
    minLevel?: LogLevel;
}

Browser/Node console with the appropriate method per level (TRACE → debug, FATAL → error).

httpTransport({ url, headers?, minLevel? })

Fire-and-forget POST to an arbitrary endpoint. No retry/batching built in — use it for prototypes or wrap it for production.

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 minLevel, filter, name.


Adapters

All adapters follow the same pattern: receive the third-party SDK as an injected parameter. logr itself never imports @sentry/*, @datadog/*, etc. — callers install those packages and pass the already-initialized SDK. This keeps logr zero-dep and makes tests trivial (mock the SDK with a plain object).

Sentry — sentryTransport(SentrySDK, options?)

import * as Sentry from '@sentry/browser';
import { sentryTransport } from '$logr/adapters/sentry';

Sentry.init({ dsn: '<your-dsn>', environment: 'prod' });

logger.addTransport(sentryTransport(Sentry, {
    minLevel: LogLevel.ERROR,          // @default ERROR
    breadcrumbLevel: LogLevel.INFO     // @default INFO
}));
  • ERROR+ → captureException(entry.error) if present, otherwise captureMessage(entry.message, severity).
  • [breadcrumbLevel, minLevel) → 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?)

import { datadogLogs } from '@datadog/browser-logs';
import { datadogTransport } from '$logr/adapters/datadog';

datadogLogs.init({
    clientToken: 'pubXXXXXXXXXX',
    site: 'datadoghq.eu',
    service: 'my-app',
    env: 'prod',
    version: '1.0.0'
});

logger.addTransport(datadogTransport(datadogLogs.logger, { minLevel: 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?)

import { Logtail } from '@logtail/browser';
import { logtailTransport } from '$logr/adapters/logtail';

const lt = new Logtail('<source-token>');
logger.addTransport(logtailTransport(lt, { minLevel: LogLevel.INFO }));

Grafana Loki — lokiTransport(options)

No SDK required — pure HTTP POST to the Loki push endpoint.

import { lokiTransport } from '$logr/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?)

import { logs } from '@opentelemetry/api-logs';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { otelTransport } from '$logr/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.).


Patterns

Sentry + breadcrumbs + metrics

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

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

if (flags.enableRemoteLogs) {
    const detach = logger.addTransport(httpTransport({ url: '...' }));
    onFlagDisable(() => detach());
}

Tests

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 minLevel/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.