UNPKG

@rollercoaster-dev/rd-logger

Version:

A neurodivergent-friendly logger for Rollercoaster.dev projects

227 lines (226 loc) 8.68 kB
import { DEFAULT_LOGGER_CONFIG, LOG_LEVEL_PRIORITY, } from './logger.config'; import { ConsoleTransport, FileTransport } from './transports'; import { TextFormatter } from './formatters'; import { formatError } from './utils'; /** * Enhanced neuro-friendly logger class */ export class Logger { constructor(options) { this.transports = []; this.config = Object.assign(Object.assign({}, DEFAULT_LOGGER_CONFIG), options); this.formatter = this.config.formatter || new TextFormatter(); // Initialize transports this.initializeTransports(); } /** * Initialize transports based on configuration */ initializeTransports() { // Clear existing transports this.transports = []; // Use custom transports if provided if (this.config.transports && this.config.transports.length > 0) { this.transports = [...this.config.transports]; } else { // Otherwise, set up default transports based on config // Always add console transport by default this.transports.push(new ConsoleTransport({ prettyPrint: this.config.prettyPrint, colorize: this.config.colorize, use24HourFormat: this.config.use24HourFormat, levelColors: this.config.levelColors, levelIcons: this.config.levelIcons, })); // Add file transport if enabled if (this.config.logToFile) { const fileTransport = new FileTransport({ filePath: this.config.logFilePath, }); // Add the transport; initialization will happen lazily on first log this.transports.push(fileTransport); } } } /** * Main logging function * @param level Log level * @param message Log message * @param context Additional context (optional) */ log(level, message, context = {}) { // Check if this log level should be shown based on configuration // In LOG_LEVEL_PRIORITY, higher values mean less verbose (debug=0, fatal=4) // So we only log if the message level value is >= the configured level value // For example, if config.level is 'info' (1), we log 'info' (1), 'warn' (2), 'error' (3), 'fatal' (4), but not 'debug' (0) if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) { return; } // Process context - handle errors specially let processedContext = Object.assign({}, context); if (context.error instanceof Error) { processedContext = Object.assign(Object.assign({}, processedContext), formatError(context.error, this.config.includeStackTrace)); delete processedContext.error; } // Get timestamp const timestamp = new Date().toISOString(); // Send to all transports for (const transport of this.transports) { transport.log(level, message, timestamp, processedContext); } } // Convenience wrappers debug(msg, ctx) { this.log('debug', msg, ctx); } info(msg, ctx) { this.log('info', msg, ctx); } warn(msg, ctx) { this.log('warn', msg, ctx); } error(msg, ctx) { this.log('error', msg, ctx); } fatal(msg, ctx) { this.log('fatal', msg, ctx); } /** * Logs an error object directly * @param msg Prefix message * @param error Error object * @param additionalContext Additional context (optional) */ logError(msg, error, additionalContext = {}) { this.log('error', msg, Object.assign(Object.assign({}, additionalContext), { error })); } /** * Explicitly log sensitive data with approval information * This method should only be used in exceptional circumstances where logging sensitive data is necessary * @param level Log level * @param message Log message * @param data Data containing sensitive information * @param approval Approval information for logging sensitive data */ logWithSensitiveData(level, message, data, approval) { // Validate approval if (!approval.reason || !approval.approvedBy) { this.warn('Attempted to log sensitive data without proper approval', { message: 'Missing required approval information. Sensitive data will not be logged.', }); return; } // Check if approval has expired if (approval.expiresAt && new Date() > approval.expiresAt) { this.warn('Attempted to log sensitive data with expired approval', { message: 'Approval has expired. Sensitive data will not be logged.', expiredAt: approval.expiresAt, }); return; } // Add approval information to the context const contextWithApproval = Object.assign(Object.assign({}, data), { __sensitive_data_approval__: { reason: approval.reason, approvedBy: approval.approvedBy, approvedAt: new Date().toISOString(), expiresAt: approval.expiresAt ? approval.expiresAt.toISOString() : undefined, } }); // Log with a warning prefix to make it stand out const warningPrefix = '⚠️ SENSITIVE DATA ⚠️ '; this.log(level, `${warningPrefix}${message}`, contextWithApproval); } /** * Convenience method for logging sensitive data with info level */ infoWithSensitiveData(message, data, approval) { this.logWithSensitiveData('info', message, data, approval); } /** * Convenience method for logging sensitive data with error level */ errorWithSensitiveData(message, data, approval) { this.logWithSensitiveData('error', message, data, approval); } /** * Update the logger's configuration dynamically * @param options Partial configuration options to update */ configure(options) { // We don't need oldConfig for now, but it might be useful for future comparisons // eslint-disable-next-line @typescript-eslint/no-unused-vars const oldConfig = Object.assign({}, this.config); this.config = Object.assign(Object.assign({}, this.config), options); // Update formatter if provided if (options.formatter) { this.formatter = options.formatter; } // Reinitialize transports if relevant config changed const transportConfigChanged = options.transports !== undefined || options.logToFile !== undefined || options.logFilePath !== undefined || options.prettyPrint !== undefined || options.colorize !== undefined || options.use24HourFormat !== undefined || options.levelColors !== undefined || options.levelIcons !== undefined; if (transportConfigChanged) { this.initializeTransports(); } } /** * Set the log level dynamically * @param level New log level to set */ setLevel(level) { this.configure({ level }); } /** * Update configuration options dynamically * @param options Partial configuration options to update * @alias configure - Provided for API consistency */ updateConfig(options) { this.configure(options); } /** * Add a transport to the logger * @param transport Transport to add */ addTransport(transport) { this.transports.push(transport); } /** * Remove a transport from the logger by name * @param name Name of the transport to remove * @returns Whether the transport was found and removed */ removeTransport(name) { const initialLength = this.transports.length; this.transports = this.transports.filter((t) => t.name !== name); return this.transports.length < initialLength; } /** * Set the formatter for the logger * @param formatter Formatter to use */ setFormatter(formatter) { this.formatter = formatter; } /** * Clean up resources used by the logger * Should be called when the logger is no longer needed */ cleanup() { // Clean up all transports for (const transport of this.transports) { if (transport.cleanup) { transport.cleanup(); } } // Clear transports array this.transports = []; } }