platform-helpers/logger.js

const Moment = require('moment');
const fse = require('fs-extra');
const crypto = require('crypto');
const colors = require('colors/safe');
const rootRequire = require('rpcm-root-require');
rootRequire('/platform-helpers/string-extensions');

/**
 * Logger class. Provides logging functionality.
 *
 * Output options provided by the class as a static enumeration
 *
 * @static
 * @requires fs-extra Native NodeJS module
 * @requires moment Native NodeJS module
 * @requires crypto Native NodeJS module
 * @requires rpcm-root-require Native NodeJS module
 * @requires string-extensions
 */

class Logger {
  /**
   * Enumeration with valid output destinations
   * @static
   * @enum {Outputs}
   * @public
   * @typedef {'CONSOLE'|'FILE'}
   * @property {string} CONSOLE Used to tell the logger to print to the console
   * @property {string} FILE Used to tell the logger to buffer the data to a log file. Log files location {{project-root}}/logger
   * @returns {object}
   */
  static Outputs = {
    CONSOLE: 'CONSOLE',
    FILE: 'FILE'
  };

  #instanceId;
  constructor () {
    this.#instanceId = Moment().format('HHmmss-SSS-') + crypto.randomBytes(16).toString('hex');
  }

  /**
   * Provides a unique identifier for the current class instance
   * @method
   * @public
   * @returns {string} A unique identifier for the current instance
   */
  getInstanceId = () => {
    return this.#instanceId;
  };

  /**
   * Logs data to selected Outputs
   *
   * When using the same class instance, FILE outputs append to the previous file when the same logName is reused
   *
   * @method
   * @public
   * @param {string} logName Log name. Generates the aggregation name. E.G.: file name composed of current timestamp and logName.
   * @param {string} data Data to log. Possible types: string, json object
   * @param {Array.<Outputs>|string.<Outputs>} [outputs=Logger.Outputs.CONSOLE] An array with expected outputs. Valid array values from class enumeration {Outputs}. Alternatively also accepts one single enumeration value.
   * @param {string} [fileExtention='log'] Provide a specific extension for the file. Affects FILE 'Outputs' only.
   * @param {boolean} [prefixEntryWithTimestamp=true] Specify if the data to log in the file should be prefixed with a timestamp. This is usefull when logging multiple entries to the same file. Affects FILE 'Outputs' only.
   * @example
   * // call library
   * const rootRequire = require('rpcm-root-require');
   * const Logger = rootRequire('/platform-helpers/logger');
   * const logger = new Logger();
   *
   * // creates a file "logName.log" in the logs folder
   * logger.log("logName", "The quick brown fox jumps over the lazy dog!");
   *
   * // creates a file "logName.log" in the logs folder
   * // content: {{timestamp}}: The quick brown fox jumps over the lazy dog!
   * logger.log("logName", "The quick brown fox jumps over the lazy dog!", Logger.Outputs.FILE, null);
   *
   * // creates a file "jsonDataLogName.json" in the logs folder
   * // content:
   * // {
   * //   "1": "a"
   * // }
   * logger.log("jsonDataLogName", '{"1":"a"}', Logger.Outputs.FILE, 'json', false);
   */
  log = (logName, data, outputs, fileExtention, prefixEntryWithTimestamp) => {
    // data validation
    if (!logName) throw new Error('First variable is required! Please provide a name for the log.');
    if (!data) throw new Error('Second variable is required! Please provide a string or an object as data.');
    if (typeof data !== 'string' && typeof data !== 'object') throw new Error('Log data provided seems to be invalid. Accepted types are strings or JSON objects.');

    // handle outputs
    const allowedOutputs = Object.keys(Logger.Outputs);
    outputs = outputs || [Logger.Outputs.CONSOLE];
    outputs = Array.isArray(outputs) ? outputs : [outputs];
    outputs = outputs.filter((el) => allowedOutputs.indexOf(el) > -1);
    if (outputs.length === 0) throw new Error('The outputs provided are invalid. Please make sure to use the valid values the Logger class provides from the enumeration "Outputs"');

    // handle file extention
    fileExtention = fileExtention || 'log';

    // print to console
    if (outputs.indexOf(Logger.Outputs.CONSOLE) > -1) {
      console.log(colors.bgRed.underline('{0} --> {1} -->'.format(Moment().format('YYYYMMDD-HHmmss.SSS'), logName)), data);
    }

    // print to a file
    if (outputs.indexOf(Logger.Outputs.FILE) > -1) {
      // stringify if its a JSON object
      if (data instanceof Error || data instanceof TypeError) {
        data = data.toString();
      } else {
        try {
          data = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
          JSON.parse(data);
          fileExtention = 'json';
        } catch (error) {}
      }

      const folderPath = '{0}/logs/{1}/{2}'.format(rootRequire.rootPath, Moment().format('YYYYMMDD'), this.#instanceId);
      const filename = '{0}.{1}'.format(logName, fileExtention);
      const fullPath = '{0}/{1}'.format(folderPath, filename);
      const output = prefixEntryWithTimestamp !== false ? '{0}: {1}'.format(Moment().format('YYYYMMDD-HHmmss.SSS'), data) : data;
      if (fse.existsSync(fullPath)) {
        fse.writeFileSync(fullPath, '\r{0}'.format(output), { flag: 'a+' });
      } else {
        fse.ensureFileSync(fullPath);
        fse.writeFileSync(fullPath, output);
      }
    }
  };
}

module.exports = Logger;