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