UNPKG

advanced-console-log

Version:

Advanced Console Log (ACL), available as the `advanced-console-log` NPM package, is a lightweight logging module for Node.js applications. It supports console and file logging with various levels, colors, and additional features such as memory usage track

1,186 lines (1,036 loc) 35.5 kB
// src/acl.js /** * ACL class for logging messages with various levels and features. * @module ACL */ const util = require("util"); const path = require("path"); const { COLORS } = require("../lib/constants"); const { formatArgs, stripAnsiCodes } = require("../lib/formatUtils"); const { getTotalHeapSizeLimit, getFormattedMemoryUsage, } = require("../lib/memoryUtils"); const { getCurrentTimestamp } = require("../lib/timestampUtils"); let FileLogger, FileLogHandler, TimerUtility, WorkerHandler, ReportGenerator; /** * Class representing a logger with multiple features like console/file logging, * colored output, memory usage tracking, caller info, and more. */ class ACL { /** * Creates an instance of the Advanced Console Logger. * @param {Object} config - Configuration options for the logger. * @param {string} config.mode - Logging mode ("regular", "async", "async-queue", or "worker"). * @param {number} [config.logLevel=1] - Console log level (0 = debug, 5 = fatal). * @param {boolean} [config.includeTimestamps=true] - Include timestamps in logs. * @param {boolean} [config.includeMemoryUsage=false] - Include memory usage info. * @param {number} [config.memoryUpdateInterval=1000] - Frequency of memory checks in ms * @param {number} [config.memoryDisplayMode=1] - Memory display format (1 = MB, 2 = %, 3 = both). * @param {boolean} [config.includeCallerInfo=false] - Include caller info in logs. * @param {number} [config.callerInfoLevel=2] - Log level for including caller info. * @param {number} [config.callerInfoDisplayMode=1] - Display mode for caller info. * @param {boolean} [config.includeInlineCallerInfo=false] - Include inline caller info in logs. * @param {number} [config.inlineCallerInfoLevel=1] - Log level for inline caller info. * @param {boolean} [config.includeStackTrace=false] - Include stack trace for errors. * @param {string} [config.timestampFormat="HH:mm:ss.SSS"] - Timestamp format. * @param {Object} [config.color] - Custom colors for log levels. * @param {boolean} [config.extraSpace=false] - Adds extra space after each log message. * @param {boolean} [config.generateReport=false] - Generate a report of log usage. * @param {boolean} [config.terminateOnFatal=false] - Terminate the process on fatal error. * @param {string} [config.outputFilename=null] - File path for file logging. * @param {number} [config.maxLogFileSizeMB=10] - Maximum size of log files in MB. * @param {number} [config.maxLogFiles=5] - Maximum number of retained log files. * @param {boolean} [config.outputFileBatchOutput=false] - Batch output to file. * @param {number} [config.outputFileBatchOutputSize=25] - Size of batch before writing to file. * @param {boolean} [config.enableTimers=false] - Enables timer functionality if set to true. */ constructor(config = {}) { this.mode = config.mode || "regular"; // Set default mode this.enableExitHandlers = config.enableExitHandlers || false; this.logLevel = typeof config.logLevel === "number" ? config.logLevel : 1; this.includeTimestamps = config.includeTimestamps !== false; this.includeMemoryUsage = config.includeMemoryUsage || false; this.memoryDisplayMode = config.memoryDisplayMode ?? 1; this.includeCallerInfo = config.includeCallerInfo || false; this.callerInfoLevel = config.callerInfoLevel ?? 2; this.callerInfoDisplayMode = config.callerInfoDisplayMode ?? 1; this.includeInlineCallerInfo = !!config.includeInlineCallerInfo; this.inlineCallerInfoLevel = config.inlineCallerInfoLevel ?? 1; this.includeStackTrace = !!config.includeStackTrace; this.timestampFormat = config.timestampFormat || "HH:mm:ss.SSS"; this.color = { debug: config.color?.debug || COLORS.CYAN, log: config.color?.log || COLORS.GREEN, info: config.color?.info || COLORS.LIGHT_GREEN, warn: config.color?.warn || COLORS.YELLOW, error: config.color?.error || COLORS.LIGHT_RED, fatal: config.color?.fatal || COLORS.MAGENTA, caller: config.color?.caller || COLORS.LIGHT_MAGENTA, inlineCaller: COLORS.LIGHT_CYAN, position: COLORS.CYAN, }; this.space = config.extraSpace ? "\n" : ""; this.generateReport = !!config.generateReport; this.terminateOnFatal = !!config.terminateOnFatal; this.memoryUpdateInterval = config.memoryUpdateInterval || 1000; this.firstShown = false; this.timers = {}; this.logEventCount = 0; this.reportData = { debug: 0, log: 0, info: 0, warn: 0, error: 0, fatal: 0, }; this.memoryUsage = ""; this.cwd = process.cwd(); this.currentFileName = __filename.replace(this.cwd, "").replace(/^\\/, ""); if (this.includeMemoryUsage) { this._totalHeapSizeLimit = getTotalHeapSizeLimit(); this.startMemoryUsageUpdates(); } // Lazy load FileLogger when file logging is needed if (config.outputFilename) { this.outputFilename = config.outputFilename; this.outputFileLogLevel = typeof config.outputFileLogLevel === "number" ? config.outputFileLogLevel : 1; this._initializeFileLogger(config); // Set up different modes switch (this.mode) { case "async": //case "async-queue": this.aliasSyncToAsyncMethods(); break; case "worker": this.setupWorkerMode(config); break; default: break; } } if (this.generateReport) { this._initializeReportGenerator(); } // Lazy initialization for timers this.enableTimers = !!config.enableTimers; this.timers = null; if (this.enableTimers) { this._initializeTimers(); } if (this.enableExitHandlers) { // Register process exit handlers for automatic cleanup this._registerExitHandlers(); } // Use FinalizationRegistry to monitor for garbage collection const registry = new FinalizationRegistry((heldValue) => { heldValue.cleanup(); // Automatically call cleanup when instance is GC'd }); registry.register(this, { cleanup: () => this.close() }); } /** * Dynamically loads methods and properties from the provided utility class into this instance. * @param {class} SupportClass - Class containing methods and properties to be loaded. */ loadMethodsAndProperties(SupportClass) { const supportClass = new SupportClass(); // Get all properties and methods from the utility class, including inherited ones Object.getOwnPropertyNames(SupportClass.prototype).forEach( (propertyName) => { if (propertyName !== "constructor") { // Check if it's a method and bind it if (typeof supportClass[propertyName] === "function") { this[propertyName] = supportClass[propertyName].bind(this); } } } ); // Get all instance variables from the utility class Object.keys(supportClass).forEach((variableName) => { if (typeof supportClass[variableName] !== "function") { this[variableName] = supportClass[variableName]; } }); } _initializeReportGenerator() { if (!ReportGenerator) { ReportGenerator = require("./ReportGenerator"); } this.reportGenerator = this.generateReport ? new ReportGenerator() : null; } /** * Initializes the FileLogger instance lazily and sets up appropriate mode configurations. * Handles both synchronous and asynchronous modes, ensuring only one logger setup. * * @param {Object} config - Logger configuration. */ _initializeFileLogger(config) { if (!FileLogger) { FileLogger = require("./FileLogger"); // Lazy load the FileLogger class } if (!FileLogHandler) { FileLogHandler = require("../handlers/FileLogHandler"); // Lazy load the FileLogHandler class } // Instantiate the FileLogger and FileLogHandler with the provided configurations this.fileLogger = new FileLogger(config, (errorMessage) => this.error(true, errorMessage) ); // Create the appropriate handler based on the current mode this.fileLogHandler = new FileLogHandler( this.fileLogger, this.mode, config.queueBatchSize || 10, config.flushInterval || 50, // Added to handle default flush interval (errorMessage) => this.error(true, errorMessage) ); // Adjust logging methods for async and queue modes if (this.mode === "async" || this.mode === "async-queue") { this.aliasSyncToAsyncMethods(); } } logWithFileLogger(message) { if (this.fileLogHandler) { this.fileLogHandler.log(message); } } /** * Set up the worker-based logging mode using worker_threads. * @param {object} config - Configuration for worker mode. */ setupWorkerMode(config) { if (!WorkerHandler) { WorkerHandler = require("../handlers/WorkerHandler"); // Lazy load the WorkerHandler class } // Ensure the config object is complete and has all necessary fields const completeConfig = { outputFilename: config.outputFilename || "default.log", maxLogFileSizeMB: config.maxLogFileSizeMB || 5, maxLogFiles: config.maxLogFiles || 3, includeTimestamps: config.includeTimestamps !== false, writeHeader: false, ...config, // Include any other properties }; // Initialize the worker with the complete configuration this.workerHandler = new WorkerHandler(completeConfig); this.workerHandler._initializeWorker(); } /** * Log messages using worker threads. * @param {string} message - The log message to send to the worker. */ logWithWorker(message) { if (this.workerHandler) { this.workerHandler.logToWorker(message); } else { console.error("Worker not _initialized. Cannot log message."); } } /** * Flushes any pending logs in the FileLogger. * Ensures that all batched or queued logs are written out based on the logging mode. * Uses synchronous or asynchronous flushing based on `mode` */ async flushFileLogs() { if (this.fileLogHandler) { try { if (this.mode === "async" || this.mode === "async-queue") { await this.fileLogHandler.flushQueue(); // Ensure async queue is flushed } else { this.fileLogger.flush(); // Synchronous flush } } catch (err) { this.error(true, `ACL: Error flushing file logs: ${err.message}`); } } } /** * Closes the FileLogger and the FileLogHandler, ensuring all pending logs are written. * Uses synchronous or asynchronous closing based on the logging mode. * This method should be the only one handling FileLogger closure to avoid conflicts. */ async closeFileLogger() { if (this.fileLogHandler) { try { await this.fileLogHandler.close(); // Wait for the handler to close properly } catch (err) { this.error(true, `ACL: Error closing FileLogHandler: ${err.message}`); } this.fileLogHandler = null; // Reset handler reference after closing } if (this.fileLogger) { try { await this.fileLogger.closeAsync(); // Ensure all pending writes are flushed before closing } catch (err) { this.error(true, `ACL: Error closing FileLogger: ${err.message}`); } this.fileLogger = null; // Reset logger reference after closing } } /** * Closes the ACL logger instance, ensuring all async operations and pending logs are completed. * This method handles the complete shutdown process, including any FileLogger and Worker cleanup. * * @returns {Promise<void>} Resolves when the logger is fully closed. */ async close() { // Guard clause to prevent redundant close calls if (this.isClosing || this.isClosed) { return; } // Mark the logger as closing to prevent new logs from being processed this.isClosing = true; try { if (includeMemoryUsage) { this.stopMemoryUsageUpdates(); } // Flush and close file logs if enabled if (this.fileLogHandler) { await this.flushFileLogs(); } // Ensure proper shutdown of the file logger and log handler await this.closeFileLogger(); // If using worker mode, shut down the worker properly if (this.workerHandler) { await this.workerHandler.closeWorker(); // Ensure worker cleanup } this.isClosed = true; // Mark as closed once all operations complete } catch (err) { console.error(`ACL: Error during close operation: ${err.message}`); } finally { this.isClosing = false; // Reset closing flag to prevent deadlocks } } // Automatically clean up on process exit or garbage collection _registerExitHandlers() { // Handle process exit events for automatic worker cleanup process.on("exit", () => { this.close(); }); process.on("SIGINT", () => { this.close(); process.exit(); // Ensure a clean exit }); process.on("uncaughtException", (err) => { console.error(`Uncaught Exception: ${err.message}`); this.close(); process.exit(1); // Exit with failure }); process.on("unhandledRejection", (reason, promise) => { console.error(`Unhandled Rejection at: ${promise} reason: ${reason}`); this.close(); process.exit(1); }); } /** * Alias synchronous methods to their asynchronous counterparts if `useAsyncLogging` is enabled. */ aliasSyncToAsyncMethods() { this.debug = this.debugAsync; this.log = this.logAsync; this.info = this.infoAsync; this.warn = this.warnAsync; this.error = this.errorAsync; this.fatal = this.fatalAsync; } /** * Retrieves the singleton instance of the Log. * If the instance does not exist, it creates a new one with the provided configuration. * @param {Object} config - Configuration object. * @returns {Log} - The singleton instance of the Log. */ static getInstance(config, name = "default") { if (!ACL.instance) { ACL.instance = {}; } if (!ACL.instance[name]) { ACL.instance[name] = new ACL(config); } return ACL.instance[name]; } /** * Retrieve caller information (file name, function name, line number, and column number). * @returns {string} - Formatted caller information with indentation showing call hierarchy. */ getCallerInfo(displayMode = 1) { // Save the original prepareStackTrace function to restore later const originalPrepareStackTrace = Error.prepareStackTrace; // Override prepareStackTrace to get the stack as an array of call sites Error.prepareStackTrace = (err, stack) => stack; // Create a new error to capture the stack trace const err = new Error(); // Get the stack trace const stack = err.stack; // Restore the original prepareStackTrace Error.prepareStackTrace = originalPrepareStackTrace; // Skip internal files (use a set to list them) const internalFiles = new Set([__filename]); let shouldLog = false; const formattedStack = []; let visibleFrameCount = 0; for (const frame of stack) { const callerFile = frame.getFileName(); const callerFunction = frame.getFunctionName() || "anonymous function"; const lineNumber = frame.getLineNumber(); const columnNumber = frame.getColumnNumber(); // Skip frames that do not have file names (e.g., internal calls) if (!callerFile) continue; // Skip internal files from the logger itself if (internalFiles.has(callerFile)) continue; // Set `shouldLog` when we're out of the internal logger files if (!shouldLog) shouldLog = true; // If still within internal files, skip this frame if (!shouldLog) continue; // Increment visible frame count for indentation visibleFrameCount++; // Shorten the file path by removing the current working directory const relativeFilePath = path.relative(this.cwd, callerFile); // Format the current stack frame based on display mode let formattedFrame; if (displayMode === 1) { // Regular display mode const indentation = " ".repeat(visibleFrameCount); formattedFrame = `${indentation}File: ${relativeFilePath}\n${indentation}Function: ${callerFunction}\n${indentation}Line: ${lineNumber}, Column: ${columnNumber}\n`; } else { // Compressed display mode formattedFrame = ` ${callerFunction} (${relativeFilePath}:${lineNumber}:${columnNumber})`; } // Add formatted frame to the stack formattedStack.push(formattedFrame); } // Join the formatted stack frames into a single string return `\n\n${formattedStack.join("\n")}\n`; } /** * Retrieve inline caller information based on the specified detail level. * This method is now directly a part of the ACL class. * @param {number} level - The detail level for the inline caller information. * @returns {string} - Formatted inline caller information. */ getInlineCallerInfo(level = 1) { // Save the original prepareStackTrace function to restore later const originalPrepareStackTrace = Error.prepareStackTrace; // Override prepareStackTrace to get the stack as an array of call sites Error.prepareStackTrace = (err, stack) => stack; // Create a new error to capture the stack trace const err = new Error(); // Get the stack trace const stack = err.stack; // Restore the original prepareStackTrace Error.prepareStackTrace = originalPrepareStackTrace; const internalFiles = [__filename]; // Use __filename to refer to the current class file for (const frame of stack) { const callerFile = frame.getFileName(); // Skip frames that do not have file names (e.g., internal calls) if (!callerFile) continue; // Check if this file is one of the internal files and skip it if so if ( internalFiles.some((internalFile) => callerFile.includes(internalFile)) ) continue; // We found the first external frame, get the caller info const callerFunction = frame.getFunctionName() || "anonymous function"; const lineNumber = frame.getLineNumber(); const columnNumber = frame.getColumnNumber(); // Shorten the file path by removing the current working directory const relativeFilePath = require("path").relative( process.cwd(), callerFile ); // Format based on the specified level switch (level) { case 1: return `${relativeFilePath}:`; case 2: return `${relativeFilePath} ${this.color.position}(${lineNumber}, ${columnNumber})${this.color.inlineCaller}:`; case 3: return `${relativeFilePath} ${this.color.position}(${lineNumber}, ${columnNumber})${this.color.inlineCaller} > ${callerFunction}:`; default: return ""; } } // If no valid external frame is found, return an empty string return ""; } /** * Determine if a message should be logged to the console. * @param {boolean} condition - The condition for logging. * @param {number} level - The level of the log message. * @returns {boolean} - Whether to log to console. */ shouldLogToConsole(condition, level) { if (typeof condition === "boolean" && !condition) { return false; } return level >= this.logLevel; } /** * Determine if a message should be logged to the file. * @param {boolean} condition - The condition for logging. * @param {number} level - The level of the log message. * @returns {boolean} - Whether to log to file. */ shouldLogToFile(condition, level) { if ( !this.outputFilename || (typeof condition === "boolean" && !condition) ) { return false; } return level >= this.outputFileLogLevel; } /** * Starts a timer to update memory usage at a regular interval. */ startMemoryUsageUpdates() { if (!this.memoryTimer && this.includeMemoryUsage) { this.memoryUsage = this.getFormattedMemoryUsage(); this.memoryTimer = setInterval(() => { this.memoryUsage = this.getFormattedMemoryUsage(); }, this.memoryUpdateInterval); } } /** * Stops the memory usage updates when the logger is closed or memory tracking is disabled. */ stopMemoryUsageUpdates() { if (this.memoryTimer) { clearInterval(this.memoryTimer); this.memoryTimer = null; } } /** * Determines and returns the formatted caller information based on the current configuration. * * @param {number} level - The log level of the current message. * @returns {string} The formatted caller information to be included in the log. */ getFormattedCallerInfo(level) { if (this.includeCallerInfo && level >= this.callerInfoLevel) { return this.getCallerInfo(); } return ""; } getLogLevelName(level) { switch (level) { case 0: return "DEBUG"; case 1: return "LOG"; case 2: return "INFO"; case 3: return "WARN"; case 4: return "ERROR"; case 5: return "FATAL"; default: return "UNKNOWN"; } } /** * Log a message to the console and/or file based on the provided conditions and levels. * @param {string} color - Message color for console output. * @param {boolean} [condition=true] - Condition to determine if the message should be logged. * @param {number} level - Log level for the message. * @param {...any} args - Arguments for the log message. */ logWithColorAndCondition(color, condition = true, level, logLevel, ...args) { const shouldLogToConsole = this.shouldLogToConsole(condition, level); const shouldLogToFile = this.fileLogger && this.shouldLogToFile(condition, level); if (!shouldLogToConsole && !shouldLogToFile) return; const formattedArgs = formatArgs( condition === true ? args : [condition, ...args], COLORS.RESET ); let stackTrace = ""; const isErrorOrFatal = level >= 3; const hasExistingStackTrace = formattedArgs.some( (arg) => typeof arg === "string" && arg.includes("Error") && arg.includes("at ") ); if (this.includeStackTrace && isErrorOrFatal && !hasExistingStackTrace) { stackTrace = new Error().stack; } const timestamp = this.includeTimestamps ? `${COLORS.LIGHT_BLUE}${getCurrentTimestamp(this.timestampFormat)}${ COLORS.RESET } ` : ""; const inlineCallerInfo = this.includeInlineCallerInfo && level >= 1 ? `${this.color.inlineCaller}${this.getInlineCallerInfo( this.inlineCallerInfoLevel )}${COLORS.RESET} ` : ""; const callerInfo = this.getFormattedCallerInfo(level); this.logEventCount++; const formattedMessage = formattedArgs.join(" ").trim(); const consoleMessage = `${timestamp}${ this.memoryUsage }${inlineCallerInfo}${color}${formattedMessage}${COLORS.RESET}${ this.space }${callerInfo}\n${stackTrace ? stackTrace + "\n" : ""}`; const logLevelString = `[${this.getLogLevelName(logLevel)}]`; const fileMessage = `${stripAnsiCodes(timestamp)}${logLevelString} ${ inlineCallerInfo ? stripAnsiCodes(inlineCallerInfo) : "" }${stripAnsiCodes(formattedMessage)}${this.space}${ callerInfo ? stripAnsiCodes(callerInfo) : "" }\n${stackTrace ? stripAnsiCodes(stackTrace) + "\n" : ""}`; if (shouldLogToConsole) { process.stdout.write(consoleMessage); } if (this.mode === "worker") { this.logWithWorker(fileMessage); } else if (shouldLogToFile) { this.logWithFileLogger(fileMessage); } } /** * Logs a message asynchronously with a specific color and condition to console and/or file. * Supports batched logging if `outputFileBatchOutput` is enabled. * @param {string} color - The color code for the log message in the console. * @param {boolean} [condition=true] - Condition to determine if the log message should be printed. * @param {number} level - The log level of the message (e.g., `0` for debug, `1` for log, etc.). * @param {...any} args - The log message arguments to be formatted and printed. */ logWithColorAndConditionAsync( color, condition = true, level, logLevel, ...args ) { const shouldLogToConsole = this.shouldLogToConsole(condition, level); const shouldLogToFile = this.fileLogger && this.shouldLogToFile(condition, level); if (!shouldLogToConsole && !shouldLogToFile) return; (async () => { const formattedArgs = formatArgs( condition === true ? args : [condition, ...args], COLORS.RESET ); const timestamp = this.includeTimestamps ? `${COLORS.LIGHT_BLUE}${getCurrentTimestamp(this.timestampFormat)}${ COLORS.RESET } ` : ""; const inlineCallerInfo = this.includeInlineCallerInfo && level >= 1 ? `${this.color.inlineCaller}${this.getInlineCallerInfo( this.inlineCallerInfoLevel )}${COLORS.RESET} ` : ""; // Get formatted caller info const callerInfo = this.getFormattedCallerInfo(level); this.logEventCount++; const formattedMessage = formattedArgs.join(" ").trim(); const consoleMessage = `${timestamp}${this.memoryUsage}${inlineCallerInfo}${color}${formattedMessage}${COLORS.RESET}${this.space}${callerInfo}\n`; const logLevelString = `[${this.getLogLevelName(logLevel)}]`; const fileMessage = `${stripAnsiCodes(timestamp)}${logLevelString} ${ inlineCallerInfo ? stripAnsiCodes(inlineCallerInfo) : "" }${stripAnsiCodes(formattedMessage)}${this.space}${ callerInfo ? stripAnsiCodes(callerInfo) : "" }\n`; if (shouldLogToConsole) { process.stdout.write(consoleMessage); } if (this.mode === "worker") { this.logWithWorker(fileMessage); } else if (shouldLogToFile) { this.logWithFileLogger(fileMessage); } })(); } /** * ACL a debug message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ debug(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("debug"); } this.logWithColorAndCondition(this.color.debug, condition, 1, 0, ...args); } /** * ACL a debug message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async debugAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("debugAsync"); } await this.logWithColorAndConditionAsync( this.color.debug, condition, 1, 0, ...args ); } /** * ACL a regular message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ log(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("log"); } this.logWithColorAndCondition(this.color.log, condition, 1, 1, ...args); } /** * ACL a regular message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async logAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("logAsync"); } await this.logWithColorAndConditionAsync( this.color.log, condition, 1, 1, ...args ); } /** * ACL an info message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ info(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("info"); } this.logWithColorAndCondition(this.color.info, condition, 1, 2, ...args); } /** * ACL an info message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async infoAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("infoAsync"); } await this.logWithColorAndConditionAsync( this.color.info, condition, 1, 2, ...args ); } /** * ACL a warning message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ warn(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("warn"); } this.logWithColorAndCondition(this.color.warn, condition, 2, 3, ...args); } /** * ACL a warning message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async warnAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("warnAsync"); } await this.logWithColorAndConditionAsync( this.color.warn, condition, 2, 3, ...args ); } /** * ACL an error message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ error(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("error"); } this.logWithColorAndCondition(this.color.error, condition, 3, 4, ...args); } /** * ACL an error message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async errorAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("errorAsync"); } await this.logWithColorAndConditionAsync( this.color.error, condition, 3, 4, ...args ); } /** * ACL a fatal message. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ fatal(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("fatal"); } this.logWithColorAndCondition(this.color.fatal, condition, 3, 5, ...args); if (this.terminateOnFatal) { process.exit(1); // Exit the process with an error code } } /** * ACL a fatal message asynchronously. * @param {boolean} [condition=true] - Condition to check before logging. * @param {...any} args - The message arguments. */ async fatalAsync(condition = true, ...args) { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.incrementLogCount === "function" ) { this.reportGenerator.incrementLogCount("fatalAsync"); } await this.logWithColorAndConditionAsync( this.color.fatal, condition, 3, 5, ...args ); if (this.terminateOnFatal) { await this.close(); // Gracefully handle all shutdown procedures setImmediate(() => process.exit(0)); // Exit with code 0 after cleanup } } /** * Initializes the TimerUtility instance if timers are enabled. */ _initializeTimers() { if (!TimerUtility) { TimerUtility = require("./TimerUtility"); } this.timers = new TimerUtility(); } /** * Start a timer with a specific label if timers are enabled. * @param {string} label - The label for the timer. */ startTimer(label) { if (this.enableTimers && this.timers) { this.timers.startTimer(label); this.debug(true, `Timer started: '${label}'`); } else { throw new Error( "ACL Timer Error: The 'enableTimers' config option must be set to true to use timers." ); } } /** * Stop a timer with a specific label and log the elapsed time if timers are enabled. * @param {string} label - The label for the timer. */ stopTimer(label) { if (this.enableTimers && this.timers) { const elapsedTime = this.timers.stopTimer(label); if (elapsedTime !== null) { this.debug( true, `Timer stopped: '${label}'. Elapsed time: ${elapsedTime}` ); } else { this.error(true, `No such timer: ${label}`); } } else { throw new Error( "ACL Timer Error: The 'enableTimers' config option must be set to true to use timers." ); } } /** * Get the elapsed time for a specific timer label if timers are enabled. * @param {string} label - The label for the timer. * @returns {number|null} - The elapsed time in milliseconds or null if timer doesn't exist. */ getTimer(label) { if (this.enableTimers && this.timers) { return this.timers.getTimer(label); } else { throw new Error( "ACL Timer Error: The 'enableTimers' config option must be set to true to use timers." ); } } /** * Clear all active timers. */ clearAllTimers() { if (this.enableTimers && this.timers) { this.timers.clearAllTimers(); } else { throw new Error( "ACL Timer Error: The 'enableTimers' config option must be set to true to use timers." ); } } /** * Pretty print an object, similar to console.dir. * @param {Object} obj - The object to print. */ dir(obj) { process.stdout.write( `${COLORS.RESET}${util.inspect(obj, { showHidden: false, depth: null, colors: true, maxArrayLength: null, compact: false, })}\n` ); } /** * Print a stack trace, similar to console.trace. */ trace() { // Capture the original stack trace const originalPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (err, stack) => stack; const err = new Error(); const stack = err.stack; Error.prepareStackTrace = originalPrepareStackTrace; // Get the current file path to filter out ACL internal calls const internalFile = __filename; // Filter out internal ACL class frames from the stack trace const filteredStack = stack .filter( (frame) => frame.getFileName() && !frame.getFileName().includes(internalFile) ) .map( (frame) => ` at ${ frame.getFunctionName() || "anonymous" } (${frame.getFileName()}:${frame.getLineNumber()}:${frame.getColumnNumber()})` ) .join("\n"); // Log the filtered stack trace process.stdout.write(`${COLORS.RESET}Trace:\n${filteredStack}\n`); } /** * Start a timer with a specific label, similar to console.time. * @param {string} label - The label for the timer. */ time(label) { this.startTimer(label); } /** * Stop a timer with a specific label and log the elapsed time, similar to console.timeEnd. * @param {string} label - The label for the timer. */ timeEnd(label) { const elapsedTime = this.getTimer(label); if (elapsedTime !== null) { this.debug( true, `Timer '${label}' ended. Elapsed time: ${elapsedTime}ms` ); delete this.timers[label]; } else { this.error(true, `Timer '${label}' does not exist.`); } } /** * Generate and print a usage report. */ report() { if ( this.generateReport && this.reportGenerator && typeof this.reportGenerator.generateReport === "function" ) { this.reportGenerator.generateReport(); } else { throw new Error( `ACL Report Error: The 'generateReport' config option must be set to true to generate a report.` ); } } /** * Get the formatted memory usage based on the memory display mode. * @returns {string} - The formatted memory usage. */ getFormattedMemoryUsage() { return getFormattedMemoryUsage( this._totalHeapSizeLimit, this.memoryDisplayMode ); } } module.exports = ACL;