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;