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).
- 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_modulesbut this package - 5 log levels —
debug,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 propagation —
AsyncLocalStoragein Node.js; auto-injected into every log entry within a scope - Console override — replace
console.*globally, with safe fallback - Offline support —
BufferedTransportdetects browser offline state and resumes on reconnect - Tree-shakable — sub-path exports so unused transports are never bundled
- Dual ESM + CJS — works with
importandrequire - TypeScript-first — full
.d.tstypes included
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 PHP — HttpTransport 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>For projects that do use npm:
npm install jsdevloggingimport { 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 ...
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',
});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
Reconfigure all loggers at runtime (e.g. raise the log level in production).
import { configure, LogLevel } from 'jsdevlogging';
configure({ level: LogLevel.WARN });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' });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"Returns a new logger with additional context fields merged in (non-mutating).
const userLogger = logger.withContext({ userId: 42, role: 'admin' });
userLogger.info('Profile updated');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 loggingReturns 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.3Groups 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();Flush all pending transport buffers (useful before process exit).
process.on('SIGTERM', async () => {
await logger.flush();
process.exit(0);
});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 originalsANSI colors in Node.js; %c CSS injection in browser DevTools.
const logger = createLogger({ format: 'pretty', colorize: true });Structured newline-delimited JSON — ideal for log aggregators (Datadog, Loki, CloudWatch).
const logger = createLogger({ format: 'json' });
// → {"timestamp":"...","level":"info","message":"...","namespace":"root","context":{...}}No color, human-readable. Good for CI logs or file output.
const logger = createLogger({ format: 'plain' });import type { IFormatter, LogEntry } from 'jsdevlogging';
class MyFormatter implements IFormatter {
format(entry: LogEntry): string {
return `[${entry.levelName}] ${entry.message}`;
}
}Writes to the original console.* methods.
import { ConsoleTransport } from 'jsdevlogging/transports';
const transport = new ConsoleTransport({ format: 'pretty', colorize: true });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),
});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,
});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
});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();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' }),
],
});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 intercepts log entries before they reach transports. Each middleware calls next(entry) to continue the chain, or returns null to drop the entry.
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
}),
],
});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);
}
}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
}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());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';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().
npm testRuns all 152 tests across unit and integration suites with vitest.
npm run test:watchnpm run test:coverage
# Opens coverage/index.htmlnpm run typechecknpm 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)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') });
"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.mjsApache License 2.0 — see LICENSE and NOTICE.
Copyright 2026 Christian Reitbauer-Rieger