From fce3102df067e9e1328a8f5d5bf6163d391b680d Mon Sep 17 00:00:00 2001 From: Brandon Rodriguez <brodriguez8774@gmail.com> Date: Thu, 17 Dec 2020 22:52:00 -0500 Subject: [PATCH] Import updated logging logic --- src/logging.js | 409 +++++++++++++++++++++++++++++-------------------- 1 file changed, 243 insertions(+), 166 deletions(-) diff --git a/src/logging.js b/src/logging.js index d6b9d9d..98982a9 100644 --- a/src/logging.js +++ b/src/logging.js @@ -2,8 +2,14 @@ * Custom logging to create more useful output. * Simply include this file at the top of other files to get extra logging logic. * + * Log Levels: + * * 10 - DEBUG + * * 20 - INFO + * * 30 - WARNING + * * 40 - ERROR + * * https://git.brandon-rodriguez.com/javascript/custom_logger - * Version 1.0 + * Version 1.1 */ @@ -11,210 +17,281 @@ const chalk = require('chalk'); const fs = require('fs'); const path = require('path'); +const { Console } = require('console'); // User Imports. const { getCurrentDate } = require('./helper_functions.js'); /** - * Essentially "upgrades" default Javascript console output to be more informative. - * Adds DEBUG, INFO, WARNING, and ERROR output levels. - * These all display file and line number before statement. + * "Fake" logging class used exclusively to initialize our real logging class. + * Helps keep our real singleton class private. * - * The LOG output level effectively stays as-is, using JavaScripts default logging implementation. + * This is necessary because JavaScript doesn't seem to allow constructor overloading or private constructors. */ -['debug', 'info', 'warning', 'warn', 'error', 'log'].forEach((methodName) => { - - // Parse log method value. - let originalLoggingMethod = null; +class Logging { - // Correct "WARNING" output level value. - if (methodName == 'warning') { - originalLoggingMethod = console['warn']; - - } else { - // Leave all other values as-is. - originalLoggingMethod = console[methodName]; + /** + * Class Constructor. + */ + constructor() { + throw new Error('Initialize class with "Logging.init()".'); } - - console[methodName] = (firstArgument, ...otherArguments) => { - - // Magic to get our line num and file name. - let originalPrepareStackTrace = Error.prepareStackTrace; - Error.prepareStackTrace = (_, stack) => stack; - let callee = new Error().stack[1]; - Error.prepareStackTrace = originalPrepareStackTrace; - let relativeFileName = path.relative(process.cwd(), callee.getFileName()); - - // Generate and log our text to console/file. - let prefix = ''; - let console_string = ''; - let log_string = ''; - if (methodName == 'debug') { - // Handling for DEBUG level. - - // Generate and print Console text. - prefix = ` ${chalk.green('[DEBUG]')} [${relativeFileName} ${callee.getLineNumber()}] `; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); - - // Generate and print Log File text. - prefix = `${getCurrentDate()} [DEBUG] [${relativeFileName} ${callee.getLineNumber()}] `; - log_string = prefix.concat(firstArgument, ...otherArguments, '\n'); - logToFile(log_string, 'debug'); - - } else if (methodName == 'info') { - // Handling for INFO level. - - // Generate and print Console text. - prefix = ` ${chalk.blue('[INFO]')} [${relativeFileName} ${callee.getLineNumber()}] `; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); - - // Generate and print Log File text. - prefix = `${getCurrentDate()} [INFO] [${relativeFileName} ${callee.getLineNumber()}] `; - log_string = prefix.concat(firstArgument, ...otherArguments, '\n'); - logToFile(log_string, 'info'); - - } else if (methodName == 'warning' || methodName == 'warn') { - // Handling for WARNING level. - - // Generate and print Console text. - prefix = ` ${chalk.yellow('[WARN]')} [${relativeFileName} ${callee.getLineNumber()}] `; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); - - // Generate and print Log File text. - prefix = `${getCurrentDate()} [WARN] [${relativeFileName} ${callee.getLineNumber()}] `; - log_string = prefix.concat(firstArgument, ...otherArguments, '\n'); - logToFile(log_string, 'warn'); - - } else if (methodName == 'error') { - // Prepend text for ERROR level. - - // Generate and print Console text. - prefix = ` ${chalk.red('[ERROR]')} [${relativeFileName} ${callee.getLineNumber()}] `; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); - - // Generate and print Log File text. - prefix = `${getCurrentDate()} [ERROR] [${relativeFileName} ${callee.getLineNumber()}] `; - log_string = prefix.concat(firstArgument, ...otherArguments, '\n'); - logToFile(log_string, 'error'); - - } else if (methodName == 'log') { - // LOG level does not get prepend text. Nor does it save to file. - // This is basically intended for "console only" output. - prefix = ' '; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); - - } else { - // Fallback prepend text for all other cases. - prefix = ` [${methodName}] [${relativeFileName} ${callee.getLineNumber()}] `; - console_string = prefix.concat(firstArgument, ...otherArguments); - originalLoggingMethod(console_string); + /** + * Method to initialize our singleton class. + */ + static init(logging_dir) { + if ( ! Logging.instance ) { + Logging.instance = new LoggingClass(logging_dir); } - - }; -}); + return Logging.instance; + } +} /** - * Writes provided string to log files. + * Real logging class. + * + * To extend class with additional logging levels: + * * Define a new "#x_file_logger" at the top of the file, to hold logging file descriptors. + * * In the constructor, copy one of the existing "this.#x_file_logger" sections and adjust for the new log level. + * * Copy one of the existing log level methods and adjust it for the new log level. + * * Finally, if applicable, add the new LogLeveLNum handling to the "#logToFile" method. */ -function logToFile(log_string, level) { - let directory = './logging/'; - let file_location = './logging/info.log'; - let log_level = getLogLevelNum(level); - - // Attempt to create logging folder. - fs.mkdir(directory, err => { - // Ignore "dir already exists" errors. Raise all others. - if (err && err.code != 'EEXIST') { - throw err; +class LoggingClass { + + // Define what will ultimately be our log file handlers. + #debug_file_logger; + #info_file_logger; + #warning_file_logger; + #error_file_logger; + + /** + * Class Constructor. + */ + constructor(logging_dir) { + + // First validate passed logging value. Start by checking if string. + if (typeof logging_dir === 'string' || logging_dir instanceof String) { + // Is string. Validate. + logging_dir = logging_dir.trim(); + + // Check last character. + if (logging_dir.slice(-1) != '/') { + logging_dir += '/'; + } + + } else { + // Is not string. Raise error. + throw new Error('Logging class must be initialized with directory location.'); } - }); - - // Attempt to write to appropriate files, based on logging level. - if (log_level >= 40) { - // Error level logging or higher. - fs.appendFile(directory.concat('error.log'), log_string, (err) => { - if (err) { - console.log(err); + + // Attempt to create logging folder. + fs.mkdir(logging_dir, err => { + // Ignore "dir already exists" errors. Raise all others. + if (err && err.code != 'EEXIST') { throw err; } }); - } - if (log_level >= 30) { - // Warning level logging or higher. - fs.appendFile(directory.concat('warn.log'), log_string, (err) => { - if (err) { - console.log(err); - throw err; - } + + // Initialize DEBUG file logging stream. + this.#debug_file_logger = new Console({ + stdout: fs.createWriteStream(logging_dir.concat('debug.log'), {'flags': 'a'}), + stderr: fs.createWriteStream(logging_dir.concat('error.log'), {'flags': 'a'}), + ignoreErrors: true, + colorMode: false }); - } - if (log_level >= 20) { - // Info level logging or higher. - fs.appendFile(directory.concat('info.log'), log_string, (err) => { - if (err) { - console.log(err); - throw err; - } + + // Initialize INFO file logging stream. + this.#info_file_logger = new Console({ + stdout: fs.createWriteStream(logging_dir.concat('info.log'), {'flags': 'a'}), + stderr: fs.createWriteStream(logging_dir.concat('error.log'), {'flags': 'a'}), + ignoreErrors: true, + colorMode: false }); - } - if (log_level >= 10) { - // Debug level logging or higher. - fs.appendFile(directory.concat('debug.log'), log_string, (err) => { - if (err) { - console.log(err); - throw err; - } + + // Initialize WARNING file logging stream. + this.#warning_file_logger = new Console({ + stdout: fs.createWriteStream(logging_dir.concat('warning.log'), {'flags': 'a'}), + stderr: fs.createWriteStream(logging_dir.concat('error.log'), {'flags': 'a'}), + ignoreErrors: true, + colorMode: false + }); + + // Initialize ERROR file logging stream. + this.#error_file_logger = new Console({ + stdout: fs.createWriteStream(logging_dir.concat('error.log'), {'flags': 'a'}), + ignoreErrors: true, + colorMode: false }); } -} + /** + * Returns calling file and line num location. + */ + getCallerInfo() { + let originalPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = (_, stack) => stack; + let callee = new Error().stack[2]; + Error.prepareStackTrace = originalPrepareStackTrace; + let relativeFileName = path.relative(process.cwd(), callee.getFileName()); + return [relativeFileName, callee.getLineNumber()] + } -/** - * Get appropriate level of logger. Based off of Python log levels. - * https://docs.python.org/3/library/logging.html - */ -function getLogLevelNum(level) { + /** + * LOG logging method. + */ + log(firstArgument, ...otherArguments) { + // LOG level does not get prepend text. Nor does it save to file. + // This is basically intended for "console only" output. + console.log(firstArgument, ...otherArguments); + } - if (level == 'error') { - return 40; + /** + * DEBUG logging method. + */ + debug(firstArgument, ...otherArguments) { + // Get calling file name and line number. + let [relativeFileName, lineNum] = this.getCallerInfo(); + + // // Generate and print Console text. + let prefix = ` ${chalk.green('[DEBUG]')} [${relativeFileName} ${lineNum}] `; + let console_string = prefix.concat(firstArgument, ...otherArguments); + console.log(console_string); + + // Generate and print Log File text. + prefix = `${getCurrentDate()} [DEBUG] [${relativeFileName} ${lineNum}] `; + let log_string = prefix.concat(firstArgument, ...otherArguments); + this.#logToFile(log_string, 10); + } - } else if (level == 'warn') { - return 30; + /** + * INFO logging method. + */ + info(firstArgument, ...otherArguments) { + // Get calling file name and line number. + let [relativeFileName, lineNum] = this.getCallerInfo(); + + // // Generate and print Console text. + let prefix = ` ${chalk.blue('[INFO]')} [${relativeFileName} ${lineNum}] `; + let console_string = prefix.concat(firstArgument, ...otherArguments); + console.log(console_string); + + // Generate and print Log File text. + prefix = `${getCurrentDate()} [INFO] [${relativeFileName} ${lineNum}] `; + let log_string = prefix.concat(firstArgument, ...otherArguments); + this.#logToFile(log_string, 20); + } - } else if (level == 'info') { - return 20; + /** + * Alias function for WARNING level. + */ + warn(firstArgument, ...otherArguments) { + // Get calling file name and line number. + let [relativeFileName, lineNum] = this.getCallerInfo(); + + // // Generate and print Console text. + let prefix = ` ${chalk.yellow('[WARN]')} [${relativeFileName} ${lineNum}] `; + let console_string = prefix.concat(firstArgument, ...otherArguments); + console.log(console_string); + + // Generate and print Log File text. + prefix = `${getCurrentDate()} [WARN] [${relativeFileName} ${lineNum}] `; + let log_string = prefix.concat(firstArgument, ...otherArguments); + this.#logToFile(log_string, 30); + } - } else if (level == 'debug') { - return 10; + /** + * WARNING logging method. + */ + warning(firstArgument, ...otherArguments) { + // Get calling file name and line number. + let [relativeFileName, lineNum] = this.getCallerInfo(); + + // // Generate and print Console text. + let prefix = ` ${chalk.yellow('[WARN]')} [${relativeFileName} ${lineNum}] `; + let console_string = prefix.concat(firstArgument, ...otherArguments); + console.log(console_string); + + // Generate and print Log File text. + prefix = `${getCurrentDate()} [WARN] [${relativeFileName} ${lineNum}] `; + let log_string = prefix.concat(firstArgument, ...otherArguments); + this.#logToFile(log_string, 30); + } - } else { - return 0; + /** + * ERROR logging method. + */ + error(firstArgument, ...otherArguments) { + // Get calling file name and line number. + let [relativeFileName, lineNum] = this.getCallerInfo(); + + // // Generate and print Console text. + let prefix = ` ${chalk.red('[ERROR]')} [${relativeFileName} ${lineNum}] `; + let console_string = prefix.concat(firstArgument, ...otherArguments); + console.log(console_string); + + // Generate and print Log File text. + prefix = `${getCurrentDate()} [ERROR] [${relativeFileName} ${lineNum}] `; + let log_string = prefix.concat(firstArgument, ...otherArguments); + this.#logToFile(log_string, 40); } -} + /** + * Writes provided string to log files. + */ + #logToFile(log_string, log_level) { + let directory = './logging/'; + let file_location = './logging/info.log'; + + // Attempt to create logging folder. + fs.mkdir(directory, err => { + // Ignore "dir already exists" errors. Raise all others. + if (err && err.code != 'EEXIST') { + throw err; + } + }); + + // Attempt to write to appropriate files, based on logging level. + if (log_level >= 40) { + // Error level logging or higher. + this.#error_file_logger.log(log_string); + } -console.debug('Logging has been initialized.'); + if (log_level >= 30) { + // Warning level logging or higher. + this.#warning_file_logger.log(log_string); + } + + if (log_level >= 20) { + // Info level logging or higher. + this.#info_file_logger.log(log_string); + } + + if (log_level >= 10) { + // Debug level logging or higher. + this.#debug_file_logger.log(log_string); + } + } + +} /** * Simple test to make sure all loging levels work as expected. */ -function testLogLevels() { - console.debug('Debug level test'); - console.info('Info level test.'); - console.warn('Warn level test.'); - console.warning('Warning level test.'); - console.error('Error level test.'); - console.log('Log/default level test.'); +function testLogLevels(logger) { + logger.debug('DEBUG level test'); + logger.info('INFO level test.'); + logger.warn('WARN level test.'); + logger.warning('WARNING level test.'); + logger.error('ERROR level test.'); + logger.log('Log/default level test.'); } -module.exports = testLogLevels; +module.exports = { + Logging, + testLogLevels, +}; -- GitLab