@rawnodes/logger
v1.5.1
Published
Flexible Winston-based logger with AsyncLocalStorage context, level overrides, and timing utilities
Maintainers
Readme
@rawnodes/logger
Flexible Winston-based logger with AsyncLocalStorage context, level overrides, and timing utilities.
Features
- Context Propagation - Automatic context via AsyncLocalStorage
- Level Overrides - Debug specific users/modules without global log level change
- Lazy Meta - Defer metadata creation until log level check passes
- Timing Utilities - Built-in performance measurement
- Request ID - Generate and extract request IDs
- Secret Masking - Mask sensitive data in logs
- Singleton Factory - Easy setup with
createSingletonLogger() - TypeScript First - Full generic type support
Installation
pnpm add @rawnodes/logger
# or
npm install @rawnodes/loggerQuick Start
1. Create your app logger
// src/logger/app.logger.ts
import { createSingletonLogger, type LoggerContext } from '@rawnodes/logger';
export interface AppLoggerContext extends LoggerContext {
userId?: number;
requestId?: string;
}
export const AppLogger = createSingletonLogger<AppLoggerContext>();2. Initialize on startup
// src/main.ts
import { AppLogger } from './logger/app.logger.js';
AppLogger.getInstance({
level: 'info',
console: { level: 'debug' },
file: {
dirname: 'logs',
filename: 'app-%DATE%.log',
level: 'info',
datePattern: 'YYYY-MM-DD',
maxFiles: '14d',
},
});3. Use in your code
import { AppLogger } from './logger/app.logger.js';
const logger = AppLogger.for('UserService');
logger.info('User created', { userId: 123 });
logger.error('Failed to create user', { error });4. Add context in middleware
// Express middleware
app.use((req, res, next) => {
const context: AppLoggerContext = {
userId: req.user?.id,
requestId: req.headers['x-request-id'] || generateRequestId(),
};
AppLogger.getStore().run(context, () => next());
});Configuration
interface LoggerConfig {
level: string; // Default log level
console: {
level: string; // Console transport level
};
file?: {
dirname: string; // Log directory
filename: string; // Filename pattern (supports %DATE%)
level: string; // File transport level
datePattern: string; // Date pattern for rotation
zippedArchive?: boolean;
maxSize?: string; // e.g., '20m'
maxFiles?: string; // e.g., '14d'
};
}Level Overrides
Debug specific users/modules without changing global log level:
// Enable debug logging for user 123
logger.setLevelOverride({ userId: 123 }, 'debug');
// Enable debug only for 'auth' module
const authLogger = logger.child('auth');
logger.setLevelOverride({ context: 'auth' }, 'debug');
// Enable debug for specific user in specific module
logger.setLevelOverride({ context: 'auth', userId: 123 }, 'debug');
// Now only auth module logs for user 123 will include debug level
// Other users and modules still get the default level
// Remove override when done
logger.removeLevelOverride({ context: 'auth', userId: 123 });
// Or clear all overrides
logger.clearLevelOverrides();Lazy Meta
Avoid creating objects when log level doesn't allow logging:
// Object is always created (even if debug is disabled)
logger.debug('message', { heavy: computeExpensiveData() });
// Function is only called when debug level is enabled
logger.debug('message', () => ({ heavy: computeExpensiveData() }));This is useful for performance-critical code where you want debug logs but don't want to pay the cost of creating log metadata when debug is disabled.
Timing
Measure execution time:
const logger = AppLogger.getInstance();
// Manual timing
const timer = logger.time('database-query');
await db.query('SELECT ...');
logger.timeEnd(timer); // Logs: "database-query completed in 45.23ms"
// Async wrapper
const users = await logger.timeAsync('fetch-users', async () => {
return await userService.findAll();
});Request ID Utilities
import { generateRequestId, getOrGenerateRequestId } from '@rawnodes/logger';
// Generate new request ID
const requestId = generateRequestId();
// => "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
// Short format
const shortId = generateRequestId({ short: true });
// => "a1b2c3d4"
// With prefix
const prefixedId = generateRequestId({ prefix: 'req' });
// => "req-a1b2c3d4-e5f6-..."
// Extract from headers or generate new
const id = getOrGenerateRequestId(req.headers);Secret Masking
import { maskSecrets } from '@rawnodes/logger';
const masked = maskSecrets({
user: 'admin',
password: 'secret123',
apiToken: 'tok_abc123xyz',
url: 'postgres://user:pass@localhost/db',
});
// Result:
// {
// user: 'admin',
// password: '***',
// apiToken: '***',
// url: 'postgres://user:***@localhost/db'
// }Child Loggers
Create scoped loggers with automatic context:
// Get child logger with context name
const logger = AppLogger.for('PaymentService');
logger.info('Processing payment'); // [PaymentService] Processing payment
// Or from instance
const baseLogger = AppLogger.getInstance();
const childLogger = baseLogger.getChildLogger('EmailService');API Reference
createSingletonLogger<TContext>()
Creates a singleton logger factory.
Returns:
interface SingletonLogger<TContext> {
getInstance(config?: LoggerConfig): BaseLogger<TContext>;
getStore(): LoggerStore<TContext>;
for(context: string): Logger;
setLevelOverride(match: Partial<TContext>, level: string): void;
removeLevelOverride(match: Partial<TContext>): void;
getLevelOverrides(): LevelOverride<TContext>[];
clearLevelOverrides(): void;
}BaseLogger<TContext>
Main logger class with methods:
log(message, context?, meta?)info(message, context?, meta?)warn(message, context?, meta?)error(message, error?, context?)debug(message, context?, meta?)verbose(message, context?, meta?)time(label): TimertimeEnd(timer, context?): TimingResulttimeAsync<T>(label, fn, context?): Promise<T>getChildLogger(context): LoggergetStore(): LoggerStore<TContext>setLevelOverride(match, level)removeLevelOverride(match)getLevelOverrides()clearLevelOverrides()
LoggerStore<TContext>
AsyncLocalStorage wrapper:
getStore(): TContext | undefinedrun<T>(context, fn): T
Integration Examples
Express
import express from 'express';
import { AppLogger, generateRequestId } from './logger';
const app = express();
app.use((req, res, next) => {
const context = {
requestId: req.headers['x-request-id'] || generateRequestId({ short: true }),
userId: req.user?.id,
};
AppLogger.getStore().run(context, () => next());
});NestJS
import { Injectable, NestMiddleware } from '@nestjs/common';
import { AppLogger, generateRequestId } from './logger';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const context = {
requestId: req.headers['x-request-id'] || generateRequestId({ short: true }),
};
AppLogger.getStore().run(context, () => next());
}
}Telegraf (Telegram Bot)
import { Telegraf } from 'telegraf';
import { AppLogger } from './logger';
bot.use((ctx, next) => {
const context = {
telegramUserId: ctx.from?.id,
chatId: ctx.chat?.id,
};
return AppLogger.getStore().run(context, () => next());
});License
MIT
