A modern, type-safe library for logging and event notifications in JavaScript/TypeScript apps.
Practical utilities for modern projects:
- Clear logs with structured data and lazy evaluation
- Lightweight observables without full-blown streams
- Zero dependencies
- Built with TypeScript from the ground up with precise types and full type inference, while remaining lightweight and fully functional in JavaScript environments.
npm install emitnlog
- Flexible Logger with 9 severity levels and template literal magic
- Type-safe Event Notifier to broadcast events only when someone's listening
- Lazy Evaluation – compute messages and events only when needed
- Multiple Logger Targets – console, stderr, file, or no-op
- Tiny Footprint – no runtime bloat
A powerful logger inspired by RFC5424, supporting both template-literal and traditional logging.
Defines the minimum level of the log entries that are emitted.
trace - Extremely detailed debugging information
debug - Diagnostic messages
info - General informational messages (default)
notice - Normal but significant events
warning - Warning conditions
error - Error conditions
critical - System component failure
alert - Action must be taken now
emergency - System is unusable
Defines the format used to emit a log entry.
plain - One plain text line per entry, no styling.
colorful - ANSI-colored line, ideal for dev terminals.
json - One structured JSON line per entry.
unformatted-json - Compact JSON line, raw and delimiter-safe.
import { ConsoleLogger } from 'emitnlog/logger';
// Defaults to 'info'
const logger = new ConsoleLogger();
// Simple message
logger.i`Server started on port 3000`;
// Using template values
const userId = 'user123';
logger.i`User ${userId} logged in successfully`;
// With error handling
const error = new Error('Connection lost');
logger.args(error).e`Something went wrong: ${error}`;
// Complex objects are handled automatically
const data = { id: 123, items: ['a', 'b', 'c'], timestamp: new Date() };
logger.d`Request data: ${data}`;
Template logging uses lazy evaluation - values are only computed when the log level matches:
import { ConsoleLogger } from 'emitnlog/logger';
// Loggers initialized to the `warning` level
const logger = new ConsoleLogger('warning');
// This expensive calculation isn't executed because debug < warning
logger.d`Complex calculation: ${() => performExpensiveOperation()}`;
// This will be executed because error > warning
logger.e`Application error: ${() => generateErrorReport()}`;
For those who prefer the traditional approach:
import { ConsoleErrorLogger } from 'emitnlog/logger';
const logger = new ConsoleErrorLogger('debug');
// Simple static message
logger.info('Server started on port 3000');
// With arguments
const userId = 'user123';
logger.info(`User ${userId} logged in successfully`);
// With error handling
const error = new Error('Connection lost');
logger.error('Something went wrong', error);
// With lazy evaluation
logger.debug(() => `Expensive operation result: ${computeExpensiveValue()}`);
For persistent logging in Node.js environments:
import { FileLogger } from 'emitnlog/logger/node';
// Simple file logger (writes to OS temp directory if path is relative)
const logger = new FileLogger('app.log', 'debug');
logger.i`Application started at ${new Date()}`;
// Advanced configuration
const configuredLogger = new FileLogger({
filePath: '/var/log/my-app.log', // Absolute path
level: 'warning', // Only warning and above
keepAnsiColors: true, // Preserve colors in log file
omitArgs: false, // Include additional arguments
errorHandler: (err) => console.error('Logging error:', err),
});
configuredLogger.e`Database connection error: ${new Error('Connection timeout')}`;
All loggers implement the same interface, making them interchangeable:
-
ConsoleLogger
: Logs to console (stdout) with color formatting enabled by default -
ConsoleErrorLogger
: Logs to stderr with color formatting enabled by default -
FileLogger
: Logs to a file with optional configuration (Node.js only) -
OFF_LOGGER
: Discards all logs (useful for testing or quickly silencing the code)
Log to multiple destinations simultaneously:
import { tee, ConsoleLogger } from 'emitnlog/logger';
import { FileLogger } from 'emitnlog/logger/node';
// Create individual loggers
const consoleLogger = new ConsoleLogger('info');
const fileLogger = new FileLogger('/var/log/app.log');
// Combine them with tee
const logger = tee(consoleLogger, fileLogger);
// Log messages go to both console and file
logger.i`Server started successfully`;
logger.args({ requestId: '12345' }).e`Database query failed: ${new Error('Timeout')}`;
Configure logging behavior through environment variables for easy deployment-time adjustments without code changes:
Note: Supported on Node.js or on runtimes where
process.env
exposes the environment variables.
import { fromEnv } from 'emitnlog/logger/environment';
// Creates logger based on environment variables
const logger = fromEnv();
logger.i`Application started`;
Configure your logger with these environment variables:
# Logger type (required)
EMITNLOG_LOGGER=console # Use ConsoleLogger
EMITNLOG_LOGGER=console-error # Use ConsoleErrorLogger
EMITNLOG_LOGGER=file:/var/log/app.log # Use FileLogger with specified path (Node.js only)
# Log level (optional)
EMITNLOG_LEVEL=debug # Set minimum log level
# Output format (optional)
EMITNLOG_FORMAT=colorful # Use colored output
Provide defaults and fallback behavior when environment variables aren't set:
import { ConsoleLogger } from 'emitnlog/logger';
import { fromEnv } from 'emitnlog/logger/environment';
// With fallback options
const logger = fromEnv({
level: 'info', // Default level if EMITNLOG_LEVEL not set
format: 'unformatted-json', // Default format if EMITNLOG_FORMAT not set
fallbackLogger: () => new ConsoleLogger(),
});
// For development vs production
const logger = fromEnv({
level: 'debug',
fallbackLogger: (level, format) => {
// In development, default to console logging
if (process.env.NODE_ENV === 'development') {
return new ConsoleLogger(level, format);
}
// In production, require explicit configuration
return undefined; // Returns OFF_LOGGER
},
});
There are three available fromEnv
variants, depending on your runtime or preferences:
import { fromEnv } from 'emitnlog/logger/environment'; // dynamic resolution (recommended)
import { fromEnv } from 'emitnlog/logger'; // neutral-only (browser-safe)
import { fromEnv } from 'emitnlog/logger/node'; // Node-only (file support)
-
The first form (
/logger/environment
) uses conditional exports to automatically select the correct logger at runtime: it supports file logging in Node.js, and gracefully disables it in browser-safe builds. This is the recommended option for most users. -
The second form (
/logger
) is always neutral and safe to use in any environment — but does not support file loggers. -
The third form (
/logger/node
) gives you full control of Node-only features like FileLogger, and should only be used when you’re explicitly targeting Node.
A typical application setup that adapts to different environments:
# Development (.env.development)
EMITNLOG_LOGGER=console
EMITNLOG_LEVEL=debug
EMITNLOG_FORMAT=colorful
# Production (.env.production)
EMITNLOG_LOGGER=file:/var/log/app.log
EMITNLOG_LEVEL=warning
EMITNLOG_FORMAT=json
# Testing (.env.test)
EMITNLOG_LOGGER=console-error
EMITNLOG_LEVEL=error
EMITNLOG_FORMAT=plain
// app.ts - Works in all environments
import { fromEnv } from 'emitnlog/logger/environment';
const logger = fromEnv({
level: 'info', // Reasonable default
format: 'colorful', // Good for development
});
logger.i`Server starting on port ${process.env.PORT || 3000}`;
logger.w`Database connection retrying...`;
logger.e`Failed to connect to external service: ${error}`;
You can create your own logger by extending BaseLogger
:
import type { LogLevel } from 'emitnlog/logger';
import { BaseLogger, emitLine, emitColorfulLine } from 'emitnlog/logger';
class MyCustomLogger extends BaseLogger {
protected override emitLine(level: LogLevel, message: string, args: readonly unknown[]): void {
// Format the log entry using the formatter utilities
const line = emitColorfulLine(level, message);
// Do something with the formatted line and args
// e.g., send to a remote logging service
myLoggingService.send({ line, args });
}
}
Categorize and organize your logs by adding fixed prefixes to any logger:
import { ConsoleLogger, withPrefix } from 'emitnlog/logger';
const logger = new ConsoleLogger();
// Create a prefixed logger for a component
const dbLogger = withPrefix(logger, 'DB');
dbLogger.i`Connected to database`; // Logs: "DB: Connected to database"
// Create nested prefixes for hierarchical logging
const userDbLogger = withPrefix(dbLogger, 'users');
userDbLogger.w`User not found: ${userId}`; // Logs: "DB.users: User not found: 123"
// Hover over these in your IDE to see their full prefixes!
// Type of dbLogger: PrefixedLogger<'DB'>
// Type of userDbLogger: PrefixedLogger<'DB.users'>
// Errors maintain their original objects
const error = new Error('Connection failed');
dbLogger.error(error); // Logs the prefixed message while preserving the error object
// Works with all log levels and methods
dbLogger.d`Query executed in ${queryTime}ms`;
For more complex applications, you can build sophisticated prefix hierarchies:
import { ConsoleLogger, withPrefix, appendPrefix, resetPrefix } from 'emitnlog/logger';
const logger = new ConsoleLogger();
// Start with a base logger
const appLogger = withPrefix(logger, 'APP');
const serviceLogger = appendPrefix(appLogger, 'UserService');
const repoLogger = appendPrefix(serviceLogger, 'Repository');
repoLogger.i`User data saved`; // Logs: "APP.UserService.Repository: User data saved"
// Switch contexts while preserving the root logger
const apiLogger = resetPrefix(repoLogger, 'API');
const v1Logger = appendPrefix(apiLogger, 'v1');
v1Logger.i`Request processed`; // Logs: "API.v1: Request processed"
// Custom separators for different naming conventions
const moduleLogger = withPrefix(logger, 'Auth', { prefixSeparator: '/', messageSeparator: ' >> ' });
const tokenLogger = appendPrefix(moduleLogger, 'Token');
tokenLogger.i`Token validated`; // Logs: "Auth/Token >> Token validated"
Key Functions:
-
withPrefix(logger, prefix)
- Creates a new prefixed logger or extends an existing prefix chain -
appendPrefix(prefixedLogger, suffix)
- Utility to append a prefix to an existing prefixed logger -
resetPrefix(logger, newPrefix)
- Utility to replace any existing prefix with a completely new one
A simple way to implement observable patterns. Listeners only get notified when something happens — and only if they're subscribed.
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
const subscription = notifier.onEvent((msg) => {
console.log(`Received: ${msg}`);
});
notifier.notify('Hello!');
subscription.close();
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// No listeners yet, this won't execute the function
notifier.notify(() => {
console.log('This is never executed because no listeners');
return 'Hello world';
});
// Now add a listener
const subscription = notifier.onEvent((message) => console.log(message));
// This will execute the function since we have a listener
notifier.notify(() => {
console.log('This runs only when someone is listening');
return 'Hello again!';
});
// Clean up
subscription.close();
Use waitForEvent()
to get a Promise that resolves when the next event occurs, without interfering with subscribed listeners.
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// Somewhere in an async function
async function handleNextEvent() {
// This will wait until the next event is notified
const eventData = await notifier.waitForEvent();
console.log(`Received event: ${eventData}`);
}
// Wait for multiple events sequentially
async function handleMultipleEvents() {
// These will wait for two separate events in sequence
const first = await notifier.waitForEvent();
const second = await notifier.waitForEvent();
console.log(`Got two events: ${first}, ${second}`);
}
// Caution: This doesn't wait for two separate events!
// Both promises resolve with the same event
async function incorrectUsage() {
const [event1, event2] = await Promise.all([notifier.waitForEvent(), notifier.waitForEvent()]);
// event1 and event2 will be identical
}
The notifier can be created with a debounced delay for scenarios in which the events are notified too quickly.
Here's an example that uses both the logger and the event notifier:
import type { OnEvent } from 'emitnlog/notifier';
import { createEventNotifier } from 'emitnlog/notifier';
import { ConsoleLogger } from 'emitnlog/logger';
type Progress = { filename: string; percent: number };
interface Uploader {
onProgress: OnEvent<Progress>;
upload(filename: string): void;
}
class FileUploader implements Uploader {
private _logger = new ConsoleLogger('debug');
private _notifier = createEventNotifier<Progress>();
public onProgress = this._notifier.onEvent;
public upload(filename: string) {
this._logger.i`Starting upload of ${filename}`;
for (let i = 0; i <= 100; i += 25) {
this._notifier.notify(() => ({ filename, percent: i }));
this._logger.d`Progress for ${filename}: ${i}%`;
}
this._logger.i`Finished upload of ${filename}`;
}
}
const uploader = new FileUploader();
const subscription = uploader.onProgress(({ filename, percent }) => {
// your UI/render function
renderProgress(filename, percent);
});
uploader.upload('video.mp4');
subscription.close();
The invocation tracker is a focused utility for monitoring function calls — it emits detailed lifecycle events, optionally logs invocation details, and supports metadata tagging. It builds on top of the core emit/log foundation, offering structured observability without requiring external tracing systems or heavy instrumentation. It is also a great example of how to use this library!
You can use the invocation tracker to track any function, and even entire objects or class instances as shown below.
import { createInvocationTracker } from 'emitnlog/tracker';
const tracker = createInvocationTracker({ tags: [{ service: 'auth' }] });
tracker.onCompleted((invocation) => {
appLogger.i`✔ ${invocation.key.operation} completed in ${invocation.duration}ms`;
updateUI(invocation.args[0]);
});
const login = tracker.track('login', (user) => {
doLogin(user);
});
login('Cayde');
The tracker automatically handles both sync and async functions, and can maintain parent-child invocation relationships:
import { createInvocationTracker } from 'emitnlog/tracker';
import { exhaustiveCheck } from 'emitnlog/utils';
// Creates a tracker for two specific operations.
const tracker = createInvocationTracker<'saveUser' | 'createUser'>();
tracker.onInvoked((invocation) => {
const operation = invocation.key.operation;
switch (operation) {
case 'saveUser':
if (invocation.phase === 'completed' && invocation.parentKey?.operation === 'createUser') {
void loadNewUserProfile();
}
break;
case 'createUser':
if (invocation.phase === 'errored') {
void handleUserCreationError(invocation.error);
}
break;
default:
exhaustiveCheck(operation);
}
});
const saveUser = tracker.track('saveUser', async (user) => {
await db.insert(user);
});
const createUser = tracker.track('createUser', async (user) => {
await saveUser(user);
});
You can use trackMethods
to automatically wrap all (or specific) methods on an object or class instance:
import { trackMethods } from 'emitnlog/tracker';
const math = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
};
trackMethods(tracker, math); // wraps all methods
math.add(1, 2); // tracked!
import { trackMethods } from 'emitnlog/tracker';
class UserService {
createUser(name) {
return { id: 1, name };
}
deleteUser(id) {
return true;
}
}
const service = new UserService();
trackMethods(tracker, service, {
methods: ['createUser', 'deleteUser'], // optional: only track these
});
The methods are wrapped in-place and preserve their this
context. You can also use this with inherited methods or mixins.
Consult the code documentation to see how you can:
- Pass tags per operation to enrich events
- Inject a custom stack to control parent-child relationship tracking (useful for advanced tracing or test isolation)
- Track method names automatically (excluding constructor and built-ins by default)
A utility for monitoring and coordinating multiple unrelated promises — perfect for scenarios like server shutdown coordination, background task monitoring, or waiting for various async operations to complete.
import { trackPromises } from 'emitnlog/tracker';
const tracker = trackPromises();
// Track some operations
const result1 = tracker.track('fetch-user', fetchUserData());
const result2 = tracker.track('save-config', saveConfiguration());
// Wait for all tracked promises to settle
await tracker.wait();
console.log('All operations complete');
import { trackPromises } from 'emitnlog/tracker';
import { withTimeout } from 'emitnlog/utils';
const shutdownTracker = trackPromises({ logger: serverLogger });
// Track cleanup operations
shutdownTracker.track('db-close', database.close());
shutdownTracker.track('cache-flush', cache.flush());
shutdownTracker.track('server-close', server.close());
// Graceful shutdown with timeout
try {
await withTimeout(shutdownTracker.wait(), 30000);
console.log('Graceful shutdown completed');
} catch {
console.log('Shutdown timeout - forcing exit');
}
import { trackPromises } from 'emitnlog/tracker';
const tracker = trackPromises();
// Monitor promise performance
tracker.onSettled((event) => {
const status = event.rejected ? 'FAILED' : 'SUCCESS';
const label = event.label ?? 'unlabeled';
console.log(`${label}: ${event.duration}ms - ${status}`);
});
// Track operations with labels
tracker.track('api-request', apiCall());
tracker.track('data-processing', () => processData()); // More accurate timing
-
Centralized Waiting:
wait()
takes a snapshot of current promises - new promises tracked during the wait aren't included - Real-time Events: Get notified when promises settle with detailed timing and result information
- Promise Suppliers: Track functions that return promises for more accurate timing measurements
- Automatic Cleanup: Promises are automatically removed when they settle to prevent memory leaks
For scenarios where you need to prevent duplicate execution of expensive operations, use holdPromises()
instead. It provides the same API as Promise Tracker but caches operations by ID, ensuring each operation runs only once while its promise is unsettled.
import { holdPromises } from 'emitnlog/tracker';
const holder = holdPromises();
// Multiple simultaneous requests for the same operation
const [result1, result2, result3] = await Promise.all([
holder.track('user-123', () => fetchUserFromAPI(123)),
holder.track('user-123', () => fetchUserFromAPI(123)),
holder.track('user-123', () => fetchUserFromAPI(123)),
]);
// Only one API call was made, all get the same result
Use Promise Holder for caching expensive operations that might be requested multiple times (API calls, database queries), and Promise Tracker for coordinating multiple different operations (shutdown procedures, monitoring). Consult the code documentation for detailed usage examples and advanced features.
A set of helpful utilities used internally but also available for direct use:
Safe and flexible value stringification for logging:
import { stringify } from 'emitnlog/utils';
// Basic usage
stringify('hello'); // 'hello'
stringify(123); // '123'
stringify(new Date()); // '2023-04-21 12:30:45.678'
// Objects with custom options
const data = {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
};
stringify(data); // compact JSON
stringify(data, { pretty: true }); // pretty-printed JSON
// Error handling
const error = new Error('Something went wrong');
stringify(error); // 'Something went wrong'
stringify(error, { includeStack: true }); // includes stack trace
// Date formatting options
const now = new Date();
stringify(now); // '2023-04-21 12:30:45.678' (ISO format without timezone)
stringify(now, { useLocale: true }); // e.g., '4/21/2023, 12:30:45 PM' (depends on user's locale)
// Handles circular references
const circular = { name: 'circular' };
circular.self = circular;
stringify(circular); // safely handles the circular reference
The stringify
utility never throws, making it safe for all logging contexts.
Convert any value to an Error object:
import { errorify } from 'emitnlog/utils';
// Convert string to Error
const error = errorify('Something went wrong');
// Preserve existing Error objects
const originalError = new Error('Original error');
const sameError = errorify(originalError); // Returns the original Error
TypeScript utility for exhaustive switch statements:
import { exhaustiveCheck } from 'emitnlog/utils';
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Loading...';
case 'success':
return 'Operation completed';
case 'error':
return 'An error occurred';
default:
// Compile-time error if we missed a case
return exhaustiveCheck(status);
}
}
Type guard for filtering out null
and undefined
values:
import { isNotNullable } from 'emitnlog/utils';
const values: Array<string | null | undefined> = ['a', null, 'b', undefined, 'c'];
const filtered: string[] = values.filter(isNotNullable);
console.log(filtered); // ['a', 'b', 'c']
Useful when working with APIs that return possibly nullable values, or when narrowing types for safe usage.
Waits for a specified duration before continuing:
import { delay } from 'emitnlog/utils';
await delay(500); // wait 500ms
console.log('This logs after half a second');
Often useful in cooldowns, stabilization intervals, and tests.
Delays function execution until after calls have stopped for a specified period. Returns promises that resolve when the operation completes:
import { debounce } from 'emitnlog/utils';
// Basic debouncing
const debouncedSave = debounce(saveUserData, 500);
// Multiple calls - only the last executes
const promise1 = debouncedSave({ name: 'Alice' });
const promise2 = debouncedSave({ name: 'Bob' });
// After 500ms: saves Bob's data, both promises resolve with same result
// Cancel or flush pending calls
debouncedSave.cancel(); // Cancels pending execution
debouncedSave.flush(); // Executes immediately
// Advanced options
const debouncedFetch = debounce(fetchData, {
delay: 300,
leading: true, // Execute immediately on first call
waitForPrevious: true, // Wait for previous promise
accumulator: (prev, current) => [...(prev || []), ...current], // Combine arguments
});
Perfect for handling rapid user input, API calls, or file system events where you only need the final result.
Wraps a promise to enforce a timeout, optionally falling back to a value:
import { withTimeout } from 'emitnlog/utils';
const fetchCompleted = (): Promise<boolean> => {...};
const result1: boolean | undefined = await withTimeout(fetchCompleted(), 1000);
const result2: boolean | 'timeout' = await withTimeout(fetchCompleted(), 1000, 'timeout');
Returns the original promise result if it resolves in time, otherwise returns the fallback. Helpful for safe async handling in flaky environments.
Creates a promise that can be resolved or rejected later:
import { createDeferredValue } from 'emitnlog/utils';
const deferred = createDeferredValue<string>();
setTimeout(() => deferred.resolve('done'), 1000);
const result = await deferred.promise;
console.log(result); // 'done'
Useful for coordinating async operations manually, like event-driven triggers or testing deferred resolution.
Continuously runs an operation at intervals until stopped or a condition is met:
import { startPolling } from 'emitnlog/utils';
const { wait, close } = startPolling(fetchStatus, 1000, { interrupt: (result) => result === 'done', timeout: 10_000 });
const final = await wait;
Polling stops automatically on timeout or interrupt. Call close()
to stop early. Works with sync or async functions and handles exceptions safely.
See source JSDoc for full types and examples.
MIT