UNPKG

@rollercoaster-dev/rd-logger

Version:

A neurodivergent-friendly logger for Rollercoaster.dev projects

235 lines 8.68 kB
import { DEFAULT_LOGGER_CONFIG, LOG_LEVEL_PRIORITY } from "./logger.config.js"; import { ConsoleTransport, FileTransport } from "./transports/index.js"; import { TextFormatter } from "./formatters/index.js"; import { formatError } from "./utils.js"; /** * Enhanced neuro-friendly logger class */ export class Logger { config; transports = []; formatter; constructor(options) { this.config = { ...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 = { ...context }; if (context.error instanceof Error) { processedContext = { ...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, { ...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 = { ...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 = { ...this.config }; this.config = { ...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 = []; } } //# sourceMappingURL=logger.service.js.map