Skip to content

Repository files navigation

jsDevLogging

A production-ready, dependency-free JavaScript/TypeScript logging library for browser and Node.js. Drop-in replacement for console, with structured output, pluggable transports, middleware hooks, and scoped loggers.

The bit that sets it apart: a single <script> tag gets you the full library — no npm, no bundler, no build step. Drop browser.min.js (17 KB) into a PHP site, point HttpTransport at your own logs.php, and every browser-side error lands in your backend. See Browser Bundle (PHP / No Build Tool).

Features

  • Works without a build tool — one IIFE <script> bundle for PHP sites and plain HTML, with the same API as the npm package
  • Zero runtime dependencies — nothing gets pulled into your node_modules but this package
  • 5 log levelsdebug, info, warn, error, fatal, with a configurable threshold
  • Structured logging — attach metadata to every log call
  • Multiple formatters — pretty/colorized, plain text, and JSON
  • Pluggable transports — Console, HTTP (with batching + retry), WebSocket, localStorage, File (with rotation)
  • Middleware pipeline — intercept, enrich, or drop log entries before they reach transports
  • Scoped loggers — namespace-prefixed child loggers with inherited config
  • Context propagationAsyncLocalStorage in Node.js; auto-injected into every log entry within a scope
  • Console override — replace console.* globally, with safe fallback
  • Offline supportBufferedTransport detects browser offline state and resumes on reconnect
  • Tree-shakable — sub-path exports so unused transports are never bundled
  • Dual ESM + CJS — works with import and require
  • TypeScript-first — full .d.ts types included

Browser Bundle (PHP / No Build Tool)

For PHP websites and any project that can't use npm, drop in the pre-built IIFE bundle. All exports are available under the global jsDevLogging.

The two bundles are committed to this repository under dist/, so a git clone or a ZIP download of the repo already contains them — no npm install, no build step:

<!-- unminified (34 KB) — good for development -->
<script src="dist/browser.js"></script>

<!-- minified (17 KB) — use in production -->
<script src="dist/browser.min.js"></script>

Basic usage in a PHP template:

<script src="/assets/browser.min.js"></script>
<script>
  const logger = jsDevLogging.createLogger({
    transports: [
      new jsDevLogging.ConsoleTransport({ format: 'pretty' }),
      new jsDevLogging.HttpTransport({ url: '/api/logs.php' }),
    ],
  });

  logger.info('Page loaded', { page: '<?= htmlspecialchars($page_id) ?>' });
  logger.warn('Slow render', { durationMs: performance.now() });
</script>

The browser bundle includes all transports except FileTransport (Node.js only), all formatters, all middleware, and overrideConsole. The API is identical to the npm package.

Receiving the logs in PHPHttpTransport posts a JSON array of log entries, so the endpoint stays small:

<?php
// logs.php
$entries = json_decode(file_get_contents('php://input'), true);

if (!is_array($entries)) {
    http_response_code(400);
    exit;
}

$fh = fopen(__DIR__ . '/logs/browser.log', 'a');
foreach ($entries as $entry) {
    fwrite($fh, json_encode($entry, JSON_UNESCAPED_SLASHES) . PHP_EOL);
}
fclose($fh);

http_response_code(204);

Each entry carries timestamp, level, message, namespace, context, meta, and — for error logs — a structured error object with name, message, and stack.

Capturing unhandled errors:

<script src="/assets/browser.min.js"></script>
<script>
  const logger = jsDevLogging.createLogger({
    transports: [new jsDevLogging.HttpTransport({ url: '/api/logs.php' })],
  });

  jsDevLogging.overrideConsole(logger);

  window.addEventListener('error', (e) => {
    logger.error(e.message, { error: e.error, filename: e.filename, line: e.lineno });
  });

  window.addEventListener('unhandledrejection', (e) => {
    logger.error('Unhandled promise rejection', { error: e.reason });
  });
</script>

Persisting logs in localStorage (useful for debugging in the field):

<script src="/assets/browser.min.js"></script>
<script>
  const storage = new jsDevLogging.LocalStorageTransport({ key: 'myapp:logs', maxEntries: 100 });
  const logger = jsDevLogging.createLogger({ transports: [storage] });

  // Later — retrieve and upload:
  const entries = storage.getEntries();
  storage.clear();
</script>

Installation

For projects that do use npm:

npm install jsdevlogging

Quick Start

import { createLogger } from 'jsdevlogging';

const logger = createLogger();

logger.info('Server started', { port: 3000 });
logger.warn('High memory usage', { memMb: 512 });
logger.error('Request failed', { error: new Error('timeout'), url: '/api/data' });

Output (pretty/colorized by default in development):

2026-04-17T10:00:00.000Z [INFO ] Server started port=3000
2026-04-17T10:00:00.000Z [WARN ] High memory usage memMb=512
2026-04-17T10:00:00.000Z [ERROR] Request failed url="/api/data"
  Error: timeout
    at ...

API

createLogger(config?)

Returns the root logger (or a configured one). All options are optional.

import { createLogger, LogLevel } from 'jsdevlogging';

const logger = createLogger({
  level: LogLevel.DEBUG,     // minimum level to output
  format: 'pretty',          // 'pretty' | 'plain' | 'json'
  colorize: true,
  namespace: 'myApp',
});

getLogger(namespace)

Returns (or creates) a named logger. Subsequent calls with the same namespace return the cached instance.

import { getLogger } from 'jsdevlogging';

const authLogger = getLogger('auth');
const dbLogger   = getLogger('db');

authLogger.info('User logged in', { userId: 123 });
dbLogger.debug('Query executed', { sql: 'SELECT ...', durationMs: 4 });

Output:

2026-04-17T10:00:00.000Z [INFO ] auth: User logged in userId=123
2026-04-17T10:00:00.000Z [DEBUG] db:   Query executed sql="SELECT ..." durationMs=4

configure(config)

Reconfigure all loggers at runtime (e.g. raise the log level in production).

import { configure, LogLevel } from 'jsdevlogging';

configure({ level: LogLevel.WARN });

Log level methods

logger.debug('Verbose detail', { key: 'value' });
logger.info('Normal event');
logger.warn('Something unexpected');
logger.error('Something failed', { error: new Error('...') });
logger.fatal('Unrecoverable state');

The second argument is always an optional metadata object. Pass Error instances under the key error — they are automatically extracted into structured entry.error with name, message, and stack.

logger.error('DB connection failed', { error: new Error('ECONNREFUSED'), host: 'db:5432' });

logger.child(namespace, context?)

Create a scoped child logger. Logs inherit the parent's transports and config but get a namespaced prefix and optional extra context.

const requestLogger = logger.child('http', { requestId: 'abc-123' });
requestLogger.info('Incoming request', { method: 'GET', path: '/users' });
// → [INFO ] http: Incoming request requestId="abc-123" method="GET" path="/users"

logger.withContext(context)

Returns a new logger with additional context fields merged in (non-mutating).

const userLogger = logger.withContext({ userId: 42, role: 'admin' });
userLogger.info('Profile updated');

logger.setLevel(level)

Change the minimum log level at runtime for this specific logger instance.

logger.setLevel(LogLevel.ERROR);   // only ERROR and FATAL will output
logger.setLevel(LogLevel.DEBUG);   // restore verbose logging

logger.time(label)

Returns a stop function. Calling it logs the elapsed duration.

const stop = logger.time('database query');
await db.query('SELECT ...');
stop();
// → [INFO ] database query completed durationMs=12.3

logger.group(label)

Groups related log entries under a shared groupId.

const group = logger.group('checkout flow');
group.log('info', 'Cart validated', { items: 3 });
group.log('info', 'Payment processed', { amount: 99.99 });
group.end();

logger.flush()

Flush all pending transport buffers (useful before process exit).

process.on('SIGTERM', async () => {
  await logger.flush();
  process.exit(0);
});

Console Override

Replace the global console.* methods so all existing console.log calls go through the library:

import { createLogger, overrideConsole, restoreConsole } from 'jsdevlogging';

const logger = createLogger();
overrideConsole(logger);

console.log('hello');         // → routed through logger.debug()
console.error('oh no');       // → routed through logger.error()

restoreConsole();             // restore originals

Formatters

Pretty (default)

ANSI colors in Node.js; %c CSS injection in browser DevTools.

const logger = createLogger({ format: 'pretty', colorize: true });

JSON

Structured newline-delimited JSON — ideal for log aggregators (Datadog, Loki, CloudWatch).

const logger = createLogger({ format: 'json' });
// → {"timestamp":"...","level":"info","message":"...","namespace":"root","context":{...}}

Plain

No color, human-readable. Good for CI logs or file output.

const logger = createLogger({ format: 'plain' });

Custom formatter

import type { IFormatter, LogEntry } from 'jsdevlogging';

class MyFormatter implements IFormatter {
  format(entry: LogEntry): string {
    return `[${entry.levelName}] ${entry.message}`;
  }
}

Transports

Console (default)

Writes to the original console.* methods.

import { ConsoleTransport } from 'jsdevlogging/transports';

const transport = new ConsoleTransport({ format: 'pretty', colorize: true });

HTTP — with buffering and retry

Send logs to a backend endpoint in batches with automatic retry on failure.

import { HttpTransport, BufferedTransport } from 'jsdevlogging/transports';
import { createLogger } from 'jsdevlogging';

const transport = new BufferedTransport(
  new HttpTransport({ url: 'https://logs.example.com/ingest' }),
  {
    batchSize: 50,           // send when 50 entries accumulate
    flushIntervalMs: 5000,   // or every 5 seconds, whichever comes first
    maxRetries: 3,           // retry failed batches up to 3 times
    retryDelayMs: 1000,      // starting delay, doubles each attempt (exponential backoff)
    maxQueueSize: 500,       // drop oldest when buffer exceeds this
  },
);

const logger = createLogger({ transports: [transport] });

The HttpTransport also supports:

new HttpTransport({
  url: 'https://logs.example.com/ingest',
  method: 'POST',
  headers: { Authorization: 'Bearer my-token' },
  onBeforeSend: (entries) => entries.filter((e) => e.level >= LogLevel.WARN),
});

WebSocket

Stream logs to a WebSocket server with automatic reconnect.

import { WebSocketTransport } from 'jsdevlogging/transports';

const transport = new WebSocketTransport({
  url: 'wss://logs.example.com/ws',
  reconnectDelayMs: 2000,
  maxReconnectAttempts: 5,
});

File (Node.js only — not included in browser bundle)

Write to a file with automatic rotation.

import { FileTransport } from 'jsdevlogging/transports';

const transport = new FileTransport({
  filePath: './logs/app.log',
  maxSizeBytes: 10 * 1024 * 1024,   // rotate at 10 MB
  maxFiles: 5,                        // keep app.log.1 through app.log.5
});

localStorage (browser only)

Persist logs in the browser for later retrieval or upload.

import { LocalStorageTransport } from 'jsdevlogging/transports';

const transport = new LocalStorageTransport({ key: 'myapp:logs', maxEntries: 200 });

// Retrieve stored entries later:
const entries = transport.getEntries();
transport.clear();

Multiple transports

import { createLogger } from 'jsdevlogging';
import { HttpTransport, BufferedTransport, FileTransport } from 'jsdevlogging/transports';

const logger = createLogger({
  transports: [
    new BufferedTransport(new HttpTransport({ url: '/api/logs' })),
    new FileTransport({ filePath: './logs/app.log' }),
  ],
});

Custom transport

import type { ITransport, LogEntry } from 'jsdevlogging';

class SlackTransport implements ITransport {
  readonly name = 'slack';

  async write(entry: LogEntry): Promise<void> {
    if (entry.level < LogLevel.ERROR) return;
    await fetch('https://hooks.slack.com/services/...', {
      method: 'POST',
      body: JSON.stringify({ text: `[${entry.levelName}] ${entry.message}` }),
    });
  }
}

Middleware

Middleware intercepts log entries before they reach transports. Each middleware calls next(entry) to continue the chain, or returns null to drop the entry.

Built-in middleware

import { ContextMiddleware, ErrorEnrichmentMiddleware, SamplingMiddleware } from 'jsdevlogging/middleware';
import { createLogger } from 'jsdevlogging';

const logger = createLogger({
  middleware: [
    new ContextMiddleware(),              // inject async context fields
    new ErrorEnrichmentMiddleware(),      // parse stack traces into StackFrame[]
    new SamplingMiddleware({ rate: 0.1, // keep 10% of debug/info logs
      levelOverrides: { error: 1.0 }    // always keep errors
    }),
  ],
});

Custom middleware

import type { IMiddleware, LogEntry, MiddlewareNext } from 'jsdevlogging';

class RedactMiddleware implements IMiddleware {
  readonly name = 'redact';

  async process(entry: LogEntry, next: MiddlewareNext): Promise<LogEntry | null> {
    const redacted = {
      ...entry,
      context: { ...entry.context, password: '[REDACTED]' },
    };
    return next(redacted);
  }
}

Context Propagation (Node.js)

Use contextManager to attach request-scoped fields that automatically appear on every log entry within an async scope — no need to pass context through every function call.

import { contextManager, createLogger } from 'jsdevlogging';

const logger = createLogger();

// Express middleware example
app.use((req, res, next) => {
  contextManager.run({ requestId: req.headers['x-request-id'], userId: req.user?.id }, next);
});

// In any handler called within that scope:
async function getUser(id: number) {
  logger.info('Fetching user', { id });
  // → [INFO ] Fetching user requestId="abc-123" userId=42 id=1
}

Plugins

The plugin system lets you bundle reusable configuration as a named package.

import type { IPlugin, ILogger, LoggerConfig } from 'jsdevlogging';
import { SamplingMiddleware } from 'jsdevlogging/middleware';

class ProductionPlugin implements IPlugin {
  readonly name = 'production';

  install(logger: ILogger, config: LoggerConfig): void {
    config.middleware.push(new SamplingMiddleware({ rate: 0.05 }));
  }
}

// Usage
import { PluginManager, createLogger } from 'jsdevlogging';

const manager = new PluginManager();
const logger = createLogger();
manager.install(new ProductionPlugin(), logger, logger.getConfig());

Tree-Shaking

Import only what you need — unused transports and middleware are excluded from the bundle.

// Only bundles the core logger and HTTP transport:
import { createLogger } from 'jsdevlogging';
import { HttpTransport, BufferedTransport } from 'jsdevlogging/transports';

// Only bundles the formatters:
import { JsonFormatter } from 'jsdevlogging/formatters';

// Only bundles the middleware:
import { SamplingMiddleware } from 'jsdevlogging/middleware';

Environment Auto-Detection

When NODE_ENV is set, the library auto-selects sensible defaults:

NODE_ENV Format Level Colorize
development pretty DEBUG true
production json WARN false
(unset) pretty DEBUG true

Override any default by passing config to createLogger() or calling configure().


Testing the Library

Run the test suite

npm test

Runs all 152 tests across unit and integration suites with vitest.

Watch mode (re-runs on file change)

npm run test:watch

Coverage report

npm run test:coverage
# Opens coverage/index.html

Type check

npm run typecheck

Build

npm run build
# Outputs to dist/:
#   index.js / index.cjs / index.d.ts     — ESM + CJS npm package
#   transports/, middleware/, formatters/  — sub-path exports
#   browser.js                             — browser IIFE bundle (unminified)
#   browser.min.js                         — browser IIFE bundle (minified)

Smoke test after build

node -e "
  const { createLogger } = require('./dist/index.cjs');
  const logger = createLogger();
  logger.info('CJS smoke test', { ok: true });
  logger.error('Error test', { error: new Error('demo') });
"

Manual integration test in Node.js

Create a file manual-test.mjs:

import {
  createLogger,
  getLogger,
  configure,
  overrideConsole,
  restoreConsole,
  LogLevel,
} from './dist/index.js';
import { BufferedTransport, FileTransport } from './dist/transports/index.js';
import { SamplingMiddleware } from './dist/middleware/index.js';

// 1. Basic logging
const logger = createLogger({ level: LogLevel.DEBUG });
logger.debug('debug message', { key: 'value' });
logger.info('info message');
logger.warn('warning here');
logger.error('error here', { error: new Error('demo error') });

// 2. Child logger
const child = logger.child('db', { host: 'localhost' });
child.info('Connected');

// 3. Timer
const stop = logger.time('expensive op');
await new Promise((r) => setTimeout(r, 50));
stop();

// 4. Console override
overrideConsole(logger);
console.log('This goes through the logger');
restoreConsole();

// 5. Sampling middleware
configure({
  middleware: [new SamplingMiddleware({ rate: 0.5 })],
});

// 6. Flush before exit
await logger.flush();
console.log('Done.');
node manual-test.mjs

License

Apache License 2.0 — see LICENSE and NOTICE.

Copyright 2026 Christian Reitbauer-Rieger

About

A production-ready, dependency-free JavaScript/TypeScript logging library for browser and Node.js.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages