Simple, universal logger with namespace support that works everywhere - browser, Node.js, and Deno.
- Console-compatible API - Drop-in replacement for
console.log/debug/warn/error - Works everywhere - Single API for browser and server environments
- Auto-adapts - Detects environment and outputs appropriately
- Namespace support - Organize logs by module/component
- Structured logging - JSON output for log aggregation tools
- Colored output - Color shortcuts for readable logs
- Extensible - Hook into logs for batching/collection
- Tiny - Zero dependencies
npm i @marianmeres/clogdeno add jsr:@marianmeres/clogimport { createClog } from "@marianmeres/clog";
// Create logger with namespace
const clog = createClog("my-app");
// Use like console
clog.log("Hello", "world"); // [my-app] Hello world
clog.debug("Debug info"); // [my-app] Debug info
clog.warn("Warning message"); // [my-app] Warning message
clog.error("Error occurred"); // [my-app] Error occurred
// Or call directly (proxies to .log)
clog("Hello", "world"); // [my-app] Hello world
// Without namespace
const logger = createClog();
logger.log("No namespace"); // No namespace
// Return value useful for throwing
throw new Error(clog.error("Something failed"));No filtering by log level - This library intentionally does not include LOG_LEVEL filtering. If you don't want certain logs, don't write them. Use your hook to filter if really needed.
No enable/disable switches - Control what you log at the source.
Console-compatible - You can replace console.log with clog.log without changing anything else. The Logger interface is designed so that console itself satisfies it.
One API for all environments - Auto-detection means you write code once, it works everywhere.
This library operates in two fundamentally different modes based on runtime detection:
- Rich, interactive output using native browser console features
- Colored namespace labels via
%cformatting - Inline colored text with color shortcuts
- Objects displayed with expandable inspection
- Machine-friendly output by design
- ISO timestamps prepended to every line
- Structured plain text:
[timestamp] [LEVEL] [namespace] message - Optional JSON output for log aggregation tools
This is an intentional, pragmatic design decision. Server logs serve a different purpose than browser console output:
- They're consumed by log aggregators
- They're grepped, parsed, and filtered by automated tools
- They need consistent, predictable structure
- Timestamps are essential for debugging distributed systems
Fancy colors, complex formatting, and visual embellishments on the server provide no value - they actually make logs harder to process and search.
✅ Good fit if you want:
- Single API that works everywhere
- Browser logs with colors and rich formatting
- Server logs optimized for machine consumption
- JSON output for log aggregation
❌ Not the best fit if you want:
- Colorful, visually styled output in server terminals
- ASCII art, box drawing, or rich formatting in CLI tools
- The same visual experience in both environments
The Logger interface methods return any instead of string to ensure true compatibility with console:
// This works because Logger uses `any` return type
const logger: Logger = console; // ✓ console methods return void
const clog: Logger = createClog("app"); // ✓ clog methods return stringConsole methods return void, but clog returns the first argument as a string (useful for patterns like throw new Error(clog.error("msg"))). Using any as the return type allows both implementations to satisfy the same interface, enabling polymorphic use of loggers throughout your codebase.
Organize logs by module, component, or feature:
// In different modules
const authLog = createClog("auth");
const apiLog = createClog("api");
const dbLog = createClog("database");
authLog.log("User logged in"); // [auth] User logged in
apiLog.warn("Slow request"); // [api] Slow request
dbLog.error("Connection failed"); // [database] Connection failedUse withNamespace() to wrap any console-compatible logger with an additional namespace. This is useful when passing loggers to modules that want their own namespace while preserving the parent context:
import { createClog, withNamespace } from "@marianmeres/clog";
const appLog = createClog("app");
const moduleLog = withNamespace(appLog, "auth");
moduleLog.log("User logged in"); // [app] [auth] User logged in
// Deep nesting works too
const subLog = withNamespace(moduleLog, "oauth");
subLog.warn("Token expired"); // [app] [auth] [oauth] Token expired
// Works with native console
const consoleLog = withNamespace(console, "my-module");
consoleLog.error("Something failed"); // [my-module] Something failed
// Return value pattern works at any nesting depth
throw new Error(moduleLog.error("Authentication failed"));Note: When using color config, only the original logger's namespace is colored. Namespaces added via withNamespace() appear as plain text.
Browser: Pretty console output with native browser features
const clog = createClog("ui");
clog.log("Rendering", { count: 42 });
// Output: [ui] Rendering { count: 42 }
// Uses browser's console stylingServer: Structured output ready for log aggregation
const clog = createClog("api");
clog.log("Request received", { method: "GET" });
// Output: [2025-11-29T10:30:45.123Z] [INFO] [api] Request received { method: 'GET' }Enable JSON output for server logs:
// Enable JSON output globally
createClog.global.jsonOutput = true;
const clog = createClog("api");
clog.log("Request received", { method: "GET", path: "/users" });
// Output (single line):
// {"timestamp":"2025-11-29T10:30:45.123Z","level":"INFO","namespace":"api","message":"Request received","arg_0":{"method":"GET","path":"/users"}}Maps console methods to standard log levels (RFC 5424):
import { LEVEL_MAP } from "@marianmares/clog";
clog.debug("Debug"); // DEBUG
clog.log("Info"); // INFO
clog.warn("Warning"); // WARNING
clog.error("Error"); // ERRORAll log methods return the first argument as a string (typed as any for console compatibility), useful for error handling:
const clog = createClog("auth");
// Convenient error throwing
throw new Error(clog.error("Authentication failed"));
// Or for validation
const userId = validateUser() ||
throw new Error(clog.error("Invalid user"));Capture all logs across your application for batching, analytics, or remote logging:
// Set up once at app bootstrap
const logBatch = [];
createClog.global.hook = (data) => {
logBatch.push(data);
// Flush batch every 100 logs
if (logBatch.length >= 100) {
sendToLogServer(logBatch);
logBatch.length = 0;
}
};
// Now all logger instances will trigger the hook
const auth = createClog("auth");
const api = createClog("api");
auth.log("Login attempt"); // Added to batch
api.warn("Slow query"); // Added to batchHook receives normalized data:
type LogData = {
level: "DEBUG" | "INFO" | "WARNING" | "ERROR";
namespace: string | false;
args: any[];
timestamp: string; // ISO 8601 format
};Replace the default output completely:
// Global writer (affects all instances)
createClog.global.writer = (data) => {
myCustomLogSystem.write({
time: data.timestamp,
severity: data.level,
module: data.namespace,
message: data.args.join(" ")
});
};
// Instance-level writer
const clog = createClog("test", {
writer: (data) => {
console.log(`Custom: ${data.level} - ${data.args[0]}`);
}
});Add color to namespace labels in browser and Deno console:
const clog = createClog("ui", { color: "blue" });
clog.log("Button clicked");
// Output: [ui] Button clicked (namespace in blue)
const errorLog = createClog("errors", { color: "red" });
errorLog.error("Failed to load");
// Output: [errors] Failed to load (namespace in red)Colors work in browser and Deno environments (uses %c formatting). Use color: "auto" to automatically assign a consistent color based on the namespace.
For inline colored text within log messages, use the color shortcut functions:
import { createClog, red, green, blue, yellow } from "@marianmeres/clog";
// namespace "app" will be auto-colored
const clog = createClog("app", { color: "auto" });
// make some of the log messages colored
clog("Status:", green("OK"));
clog("Error:", red("Connection failed"));
clog(blue("Info:"), "Processed in", yellow("42ms"));Available colors: gray, grey, red, orange, yellow, green, teal, cyan, blue, purple, magenta, pink. All colors are optimized for readability on both light and dark backgrounds. In environments that don't support %c formatting (like Node.js), colored text is output as plain strings with no artifacts.
String concatenation also works safely: "Status:" + green("OK") outputs "Status:OK" (color is lost, but no [object Object] artifacts). For colored output, use comma-separated arguments instead.
Control whether .debug() calls produce output globally or per-instance:
// Global: disable debug for all loggers
createClog.global.debug = process.env.NODE_ENV !== "development";
const apiLog = createClog("api");
const dbLog = createClog("db");
apiLog.debug("skipped in production"); // Respects global setting
dbLog.debug("also skipped"); // Respects global setting
// Per-instance: override global setting
const verboseLog = createClog("verbose", { debug: true });
verboseLog.debug("always outputs"); // Overrides global
// Or disable for specific logger
const quietLog = createClog("quiet", { debug: false });
quietLog.debug("never outputs"); // Overrides globalPrecedence: Instance config.debug → Global createClog.global.debug → Default (true)
When debug: false, the .debug() method becomes a no-op (but still returns the first argument as a string for API consistency). All other log levels work normally regardless of this setting.
Force non-primitive arguments to be JSON.stringified, making objects visible as strings:
// Global
createClog.global.stringify = true;
// Per-instance
const clog = createClog("api", { stringify: true });
clog.log("data", { user: "john" }, [1, 2, 3]);
// Output: [timestamp] [INFO] [api] data {"user":"john"} [1,2,3]Without stringify, objects might appear as [object Object] in some contexts. With stringify: true, they're always JSON strings.
Precedence: Instance config.stringify → Global createClog.global.stringify → Default (false)
Concatenate all arguments into a single string output. This also enables stringify behavior:
// Global
createClog.global.concat = true;
// Per-instance
const clog = createClog("x", { concat: true });
clog(1, { hey: "ho" });
// Output: [timestamp] [INFO] [x] 1 {"hey":"ho"}
// Console receives exactly ONE string argumentThis is useful when you need:
- Single-line log output for easier parsing/grep
- Guaranteed flat string output (no object expansion in console)
- Integration with log systems expecting single-string messages
Precedence: Instance config.concat → Global createClog.global.concat → Default (false)
| Config | Objects | Console args |
|---|---|---|
| neither | as-is | multiple |
stringify: true |
JSON.stringify | multiple |
concat: true |
JSON.stringify | single string |
Warning: This feature is intended for local development debugging only. Do NOT use in production as capturing stack traces has significant performance overhead.
Append call stack trace to log output, showing where each log call originated:
// Global
createClog.global.stacktrace = true;
// Per-instance
const clog = createClog("debug", { stacktrace: true });
clog.log("Where am I called from?");
// Output includes stack trace as last argument showing call siteYou can also limit the number of stack frames:
// Show only top 3 frames
createClog.global.stacktrace = 3;With JSON output enabled, the stack trace is included as a "stack" field in the JSON object.
Precedence: Instance config.stacktrace → Global createClog.global.stacktrace → Default (undefined/disabled)
Inject contextual metadata (like user ID, request ID, session info) into log entries. The metadata is available in LogData.meta for custom writers and hooks, but is NOT passed to console output:
// Instance-level getMeta
const clog = createClog("api", {
getMeta: () => ({
userId: getCurrentUserId(),
requestId: getRequestId()
})
});
clog.log("Request received");
// Console output: [timestamp] [INFO] [api] Request received
// But LogData.meta contains: { userId: "...", requestId: "..." }
// Global getMeta (affects all instances)
createClog.global.getMeta = () => ({
sessionId: getSessionId(),
env: process.env.NODE_ENV
});Access metadata in custom writers or hooks:
// In a custom writer
const clog = createClog("app", {
getMeta: () => ({ traceId: "abc-123" }),
writer: (data) => {
console.log("Meta:", data.meta); // { traceId: "abc-123" }
console.log("Message:", data.args[0]);
}
});
// In a global hook for log collection
createClog.global.hook = (data) => {
sendToAnalytics({
...data,
meta: data.meta // { userId: "...", requestId: "..." }
});
};With JSON output enabled, metadata is automatically included:
createClog.global.jsonOutput = true;
createClog.global.getMeta = () => ({ userId: "user-123" });
const clog = createClog("api");
clog.log("Request");
// Output: {"timestamp":"...","level":"INFO","namespace":"api","message":"Request","meta":{"userId":"user-123"}}Key points:
getMetais called synchronously on every log call- Returns
Record<string, unknown>for flexibility - Instance
getMetaoverrides globalgetMeta - If
getMetareturnsundefined, nometafield is added toLogData
Precedence: Instance config.getMeta → Global createClog.global.getMeta → Default (undefined)
For complete API documentation, see API.md.
// Create a logger
const clog = createClog(namespace?, config?);
// Create a no-op logger (for testing)
const noop = createNoopClog(namespace?);
// Log methods (return first arg as string, typed as `any`)
clog.debug(...args); // DEBUG level
clog.log(...args); // INFO level
clog.warn(...args); // WARNING level
clog.error(...args); // ERROR level
clog(...args); // Callable, same as clog.log()
// Instance properties
clog.ns; // readonly namespace
// Nested namespaces (wrap any console-compatible logger)
const nested = withNamespace(clog, "module");
nested.log("msg"); // [original-ns] [module] msg
// Global configuration
createClog.global.hook = (data: LogData) => { /* ... */ };
createClog.global.writer = (data: LogData) => { /* ... */ };
createClog.global.jsonOutput = true;
createClog.global.debug = false; // disable debug globally
createClog.global.stringify = true; // JSON.stringify objects
createClog.global.concat = true; // single string output
createClog.global.stacktrace = true; // append call stack (dev only!)
createClog.global.getMeta = () => ({ userId: "..." }); // metadata injection
// Reset global config
createClog.reset();interface ClogConfig {
writer?: WriterFn;
color?: string | null;
debug?: boolean; // when false, .debug() is a no-op
stringify?: boolean; // JSON.stringify non-primitive args
concat?: boolean; // concatenate all args into single string
stacktrace?: boolean | number; // append call stack (dev only!)
getMeta?: () => Record<string, unknown>; // metadata injection
}
interface GlobalConfig {
hook?: HookFn;
writer?: WriterFn;
jsonOutput?: boolean;
debug?: boolean; // can be overridden per-instance
stringify?: boolean; // can be overridden per-instance
concat?: boolean; // can be overridden per-instance
stacktrace?: boolean | number; // can be overridden per-instance (dev only!)
getMeta?: () => Record<string, unknown>; // can be overridden per-instance
}
type LogData = {
level: "DEBUG" | "INFO" | "WARNING" | "ERROR";
namespace: string | false;
args: any[];
timestamp: string;
config?: ClogConfig; // instance config (for custom writers)
meta?: Record<string, unknown>; // metadata from getMeta()
};import { createClog } from "@marianmares/clog";
const clog = createClog("app");
clog.debug("Debugging info", { userId: 123 });
clog.log("User logged in");
clog.warn("Session expiring soon");
clog.error("Failed to save", new Error("DB connection lost"));// auth.ts
const authLog = createClog("auth");
authLog.log("Login attempt", { email: "user@example.com" });
// api.ts
const apiLog = createClog("api");
apiLog.warn("Rate limit approaching", { remaining: 10 });
// database.ts
const dbLog = createClog("db");
dbLog.error("Query timeout", { query: "SELECT * FROM users" });// Development: readable text logs (default)
const clog = createClog("api");
clog.log("Request received");
// [2025-11-29T10:30:45.123Z] [INFO] [api] Request received
// Production: enable JSON logs for aggregation
createClog.global.jsonOutput = true;
const clog2 = createClog("api");
clog2.log("Request received", { userId: 123 });
// {"timestamp":"2025-11-29T10:30:45.123Z","level":"INFO","namespace":"api","message":"Request received","arg_0":{"userId":123}}For tests where you want to suppress all console output, use createNoopClog:
import { createNoopClog } from "@marianmeres/clog";
// Create a silent logger - no output at all
const clog = createNoopClog("test");
clog.log("silent"); // returns "silent", outputs nothing
clog.error("fail"); // returns "fail", outputs nothing
// Return value pattern still works
throw new Error(clog.error("Something failed"));// test.ts
import { createClog } from "@marianmeres/clog";
import { assertEquals } from "@std/assert";
Deno.test("logs correct message", () => {
const captured: string[] = [];
createClog.global.writer = (data) => {
captured.push(data.args[0]);
};
const clog = createClog("test");
clog.log("Hello");
assertEquals(captured[0], "Hello");
createClog.reset(); // Clean up
});For production log batching and forwarding, use the included createLogForwarder utility:
import { createClog } from "@marianmeres/clog";
import { createLogForwarder } from "@marianmeres/clog/forward";
const forwarder = createLogForwarder(
async (logs) => {
await fetch("/api/logs", { method: "POST", body: JSON.stringify(logs) });
return true;
},
{ flushIntervalMs: 5000, flushThreshold: 50, maxBatchSize: 1000 }
);
createClog.global.hook = forwarder.hook;
// Graceful shutdown
process.on("SIGTERM", async () => {
await forwarder.drain();
process.exit(0);
});The forwarder wraps @marianmeres/batch and provides:
- Time-based flushing (
flushIntervalMs) - flush every N ms - Threshold-based flushing (
flushThreshold) - flush when buffer reaches N items - Buffer overflow protection (
maxBatchSize) - oldest items discarded if exceeded - Graceful shutdown (
drain()) - flush remaining items before exit - State monitoring (
subscribe()) - observe buffer size and flush status
Full API: hook, add, flush, drain, start, stop, reset, dump, configure, subscribe, size, isRunning, isFlushing
When using @marianmeres/clog in an application with multiple dependencies that each bundle their own copy of the library, the global configuration (createClog.global) is truly shared across all instances.
This works because the global state uses Symbol.for() + globalThis:
// Internally, clog stores global config like this:
const GLOBAL_KEY = Symbol.for("@marianmeres/clog");
const GLOBAL = (globalThis as any)[GLOBAL_KEY] ??= { /* defaults */ };This means:
- ✅ Set
createClog.global.jsonOutput = trueonce at app bootstrap - ✅ All components (even deeply nested dependencies) see that config
- ✅ A global hook captures logs from every clog instance in your app
- ✅ Works regardless of how many copies of clog exist in
node_modules
// app.ts - set once at startup
import { createClog } from "@marianmeres/clog";
createClog.global.jsonOutput = true;
createClog.global.hook = (data) => sendToAnalytics(data);
// Any dependency using @marianmeres/clog will automatically
// use JSON output and trigger your hookThe v3.0 refactor simplified the API significantly:
Removed:
createLogger()- UsecreateClog()insteadcreateClogStr()- No longer neededinfo()method - Uselog()(maps to INFO level)DISABLEDglobal flag - Remove or don't logCONFIGobject with complex flags - Simplified toglobal.jsonOutputCOLORSflag - Color now per-instance only- Chainable color API - Use config instead
- Time/dateTime options - Timestamps always in server mode
Migration examples:
// v2.x
const logger = createLogger("api", true); // JSON output
logger.log("message");
// v3.x
createClog.global.jsonOutput = true;
const logger = createClog("api");
logger.log("message");
// v2.x
const clog = createClog("ui").color("red").log("msg");
// v3.x
const clog = createClog("ui", { color: "red" });
clog.log("msg");
// v2.x
createClog.DISABLED = true;
// v3.x
// Remove or use custom writer that no-ops
createClog.global.writer = () => {};