@enclosurejs/logging
v1.1.0
Published
Structured logging with pluggable transports for Enclosure apps
Readme
@enclosurejs/logging — Structured logging with pluggable transports
[!IMPORTANT] One module — console, file, widget, and custom transports. Level filtering, child loggers with merged context, debounced file output with rotation, in-app log panel. Zero external dependencies.
The Problem
Every desktop/web app needs logging but rolls its own: inconsistent formats, no level filtering, no structured context, no file rotation, no way to show logs in the app itself. Switching platforms means rewriting transport logic.
@enclosurejs/logging solves this with createLoggingModule() — a DI module that auto-wires transports from capabilities (console, file, widget), filters by level using a single integer comparison, and provides Logger with child() for hierarchical context. File transport handles buffered writes with size-based rotation. Widget transport renders a live log panel in the app.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ createLoggingModule() │
│ │
│ options.console ≠ false → ConsoleTransport (pretty | json) │
│ options.file + FileSystemToken → FileTransport (buffered) │
│ options.widget + WidgetRegistryToken → WidgetTransport (DOM) │
│ options.transports → custom transports appended │
│ │
│ ┌──────────────┐ │
│ │ RootLogger │ │
│ │ level gate │ │
│ │ transports │ │
│ └──────┬───────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ChildLogger ChildLogger ChildLogger │
│ {module:'auth'} {module:'db'} {session:'x'} │
│ │
│ All children share root's transports + level gate. │
│ Context merges: root < child < call-site. │
└──────────────────────────────────────────────────────────────────┘Level gate: LOG_LEVEL_VALUE[entry.level] >= LOG_LEVEL_VALUE[configured] — single integer comparison, zero allocation when suppressed.
Dependency rule: @enclosurejs/core (peer). No external runtime dependencies.
Quick Start
1. Install
pnpm add @enclosurejs/logging2. Module usage
import { createApp } from '@enclosurejs/core';
import { createLoggingModule, LoggerToken } from '@enclosurejs/logging';
const app = createApp({
modules: [
createLoggingModule({
level: 'debug',
context: { app: 'my-app' },
file: true,
widget: true,
}),
],
});3. Using the logger
const logger = ctx.use(LoggerToken);
logger.info('server started', { port: 3000 });
logger.warn('deprecated API called');
logger.error('request failed', new Error('timeout'), { url: '/api/data' });
const child = logger.child({ module: 'auth' });
child.info('user logged in', { userId: 42 });
// context: { app: 'my-app', module: 'auth', userId: 42 }4. Custom transports
import { createCallbackTransport } from '@enclosurejs/logging';
const sentry = createCallbackTransport('sentry', (entry) => {
if (entry.level === 'error' || entry.level === 'fatal') {
captureException(entry.error);
}
});
createLoggingModule({ transports: [sentry] });How It Works
Log Levels
Seven levels, ordered by severity:
| Level | Value | Use case |
| -------- | ----- | ------------------------------------ |
| trace | 0 | Detailed debugging, hot paths |
| debug | 1 | Development diagnostics |
| info | 2 | Normal operational events |
| warn | 3 | Recoverable issues |
| error | 4 | Failed operations |
| fatal | 5 | Unrecoverable — app should shut down |
| silent | 6 | Gate-only: suppresses all output |
Default level: 'debug' in dev (NODE_ENV !== 'production'), 'info' in production.
Console Transport
Human-readable (pretty) or machine-readable (json) output. Pretty format: [HH:MM:SS.mmm] INF message {context}. JSON format: {"level":"info","ts":...,"msg":"...","ctx":{...}}. Maps levels to console.log/warn/error.
File Transport
Buffered JSON-lines output with size-based rotation. Entries are batched in memory and flushed on an interval (default 1s) or immediately on error/fatal for crash safety. Rotation: app.log → app.1.log → app.2.log → ... → oldest deleted. Requires FileSystemToken capability.
Widget Transport
In-app log panel via WidgetRegistry. Circular buffer of last N entries, rendered as a scrollable DOM panel with timestamps, level badges, and a level filter dropdown. Requires WidgetRegistryToken capability.
Callback Transport
Wraps a user function as a transport. Useful for forwarding to Sentry, analytics, or custom sinks.
Child Loggers
logger.child({ module: 'auth' }) creates a child that inherits the parent's transports and merges context. Creating a child is cheap (one object spread). Context priority: root < child < call-site. setLevel() on a child changes the root level (shared gate).
API
Exports (@enclosurejs/logging)
| Export | Kind | Purpose |
| ------------------------- | -------- | ---------------------------------------------------- |
| createLoggingModule | function | Module factory — auto-wires transports from DI |
| LoggerToken | token | DI token to resolve the logger |
| RootLogger | class | Logger implementation (for advanced use / testing) |
| createConsoleTransport | function | Console transport factory |
| createFileTransport | function | File transport factory (requires FileSystem) |
| createCallbackTransport | function | Callback transport factory |
| createWidgetTransport | function | Widget transport factory (requires WidgetRegistry) |
| LOG_LEVEL_VALUE | const | Numeric priority map for level comparison |
| LoggingOptions | type | Config for createLoggingModule |
| RootLoggerOptions | type | Config for RootLogger constructor |
| ConsoleTransportOptions | type | Console transport config |
| ConsoleFormat | type | 'pretty' \| 'json' |
| FileTransportOptions | type | File transport config |
| WidgetTransportOptions | type | Widget transport config |
| LogLevel | type | All 7 levels including 'silent' |
| ActiveLogLevel | type | 6 levels excluding 'silent' |
| LogEntry | type | Immutable structured log record |
| Logger | type | Logger interface |
| LogTransport | type | Transport interface (write/flush/dispose) |
LoggingOptions
| Field | Type | Required | Description |
| ------------ | ----------------------------------- | -------- | --------------------------------------------------------------- |
| level | LogLevel | No | Minimum log level. Default: 'debug' (dev) / 'info' (prod) |
| transports | LogTransport[] | No | Custom transports appended after auto-configured ones |
| console | ConsoleTransportOptions \| false | No | Console config, or false to disable. Default: enabled, pretty |
| file | FileTransportOptions \| boolean | No | File config, true for defaults, false to disable |
| widget | WidgetTransportOptions \| boolean | No | Widget config, true for defaults, false to disable |
| context | Record<string, unknown> | No | Root context fields added to every log entry |
FileTransportOptions
| Field | Type | Default | Description |
| --------------- | -------- | ----------------- | ------------------------------------- |
| dir | string | 'logs' | Directory for log files |
| filename | string | 'app.log' | Current log filename |
| maxSize | number | 5 * 1024 * 1024 | Max file size before rotation (bytes) |
| maxFiles | number | 5 | Max rotated files kept |
| flushInterval | number | 1000 | Auto-flush interval (ms) |
| bufferSize | number | 100 | Entries before forced flush |
Configuration
No config files. createLoggingModule() accepts all options inline:
// Minimal — console transport with pretty format, auto level
createLoggingModule();
// Production — JSON format, file output, error-only console
createLoggingModule({
level: 'warn',
console: { format: 'json' },
file: { dir: 'data/logs', maxSize: 10 * 1024 * 1024 },
});
// Development — all transports, debug level
createLoggingModule({
level: 'debug',
file: true,
widget: true,
context: { env: 'dev' },
});Types Exported
| Type | Used by |
| ------------------------- | ----------------------------------------- |
| LogLevel | Level configuration, filtering |
| ActiveLogLevel | Log entry level field (excludes 'silent') |
| LogEntry | Transport implementations, log consumers |
| Logger | Any code using the logger via DI |
| LogTransport | Custom transport implementations |
| LoggingOptions | Module factory configuration |
| RootLoggerOptions | Direct RootLogger construction |
| ConsoleTransportOptions | Console transport configuration |
| ConsoleFormat | Console output format selection |
| FileTransportOptions | File transport configuration |
| WidgetTransportOptions | Widget transport configuration |
Safety
Level Gate Safety
- Level check is a single integer comparison — zero allocation when suppressed
silentlevel suppresses all output includingfatalsetLevel()changes filtering at runtime without restarting
Transport Safety
- File transport catches I/O errors and re-prepends buffered lines — no data loss on transient failures
- File transport flushes immediately on
error/fatal— crash safety - Widget transport uses a circular buffer — bounded memory regardless of log volume
- All transports are flushed and disposed on module shutdown
Context Safety
- Base context is frozen at construction — immutable shared state
- Child context is frozen — no mutation after creation
- Context merges create new objects — no shared references between loggers
DI Safety
- File transport only created when
FileSystemTokenis available - Widget transport only created when
WidgetRegistryTokenis available - Missing capabilities are silently skipped — no crash on capability absence
Benchmarks
Not applicable. Logging is I/O-bound with a single integer comparison as the hot path. The level gate avoids any allocation when a message is suppressed. File transport is explicitly buffered and debounced. There is no CPU-intensive hot path to benchmark.
Bundle Size
| Output | File | Size |
| ------------ | ------------ | -------- |
| Runtime (JS) | index.js | 14.44 KB |
| Types (DTS) | index.d.ts | 6.00 KB |
| Total | | 20.44 KB |
Single entrypoint — all transports bundled. Zero runtime dependencies.
Quality
| Metric | Value |
| ---------------------- | ------------------------------------------------------------------ |
| Unit tests | 66 (all pass) |
| Test files | 6 (logger, console, file, callback, widget, module) |
| Source files | 8 (index.ts, types.ts, logger.ts, module.ts, 4 transports) |
| External dependencies | 0 |
| Peer dependencies | @enclosurejs/core |
| Coverage (logging/src) | statements 95.97%, branches 91.93%, functions 93.54%, lines 95.97% |
| Coverage (transports) | statements 93.83%, branches 87.35%, functions 100%, lines 93.83% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
66 tests logger, 4 transports, DI module integration
v8 coverage stmts 96%, branches 92%, funcs 94%, lines 96%
Layer 3: BENCHMARKS
N/A I/O-bound, integer level gate — no CPU hot path
Layer 4: PACKAGE HEALTH
0 external deps pure TypeScript + @enclosurejs/core
tsup build ESM + DTS output, single entrypointFile Structure
packages/logging/
├── src/
│ ├── index.ts Barrel: all public exports
│ ├── types.ts LogLevel, LogEntry, Logger, LogTransport, LOG_LEVEL_VALUE
│ ├── logger.ts RootLogger + ChildLogger — level gate, dispatch, context merge
│ ├── module.ts createLoggingModule(), LoggerToken — auto-wires transports from DI
│ ├── transports/
│ │ ├── console.ts Pretty or JSON console output
│ │ ├── file.ts Buffered JSON-lines with size-based rotation
│ │ ├── callback.ts User function wrapper
│ │ └── widget.ts In-app log panel via WidgetRegistry
│ └── __tests__/
│ ├── logger.test.ts 23 tests — root + child loggers, level filtering, context
│ ├── console.test.ts 11 tests — pretty/json format, level→method mapping
│ ├── file.test.ts 11 tests — buffering, rotation, crash flush, JSON-lines
│ ├── callback.test.ts 3 tests — name, handler delegation
│ ├── widget.test.ts 7 tests — registration, buffer, mount, dispose
│ └── module.test.ts 11 tests — DI, capability gating, custom transports
├── package.json
├── tsconfig.json
└── tsup.config.tsLicense
MIT
