From b3847a2e355c1c5113eb99eb1a755535bd225998 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 21 Sep 2020 01:00:19 +0200 Subject: Update the code. --- lib/cologne.js | 242 +++++++++++++++++----------------------- lib/cologne/formatter/simple.js | 67 ----------- lib/cologne/formatter/token.js | 104 ----------------- lib/cologne/log_utilities.js | 155 ------------------------- lib/cologne/logger/console.js | 95 ---------------- lib/cologne/logger/file.js | 71 ------------ lib/formatters/simple.js | 62 ++++++++++ lib/formatters/token.js | 93 +++++++++++++++ lib/loggers/console.js | 94 ++++++++++++++++ lib/loggers/file.js | 68 +++++++++++ lib/utilities.js | 169 ++++++++++++++++++++++++++++ 11 files changed, 587 insertions(+), 633 deletions(-) delete mode 100644 lib/cologne/formatter/simple.js delete mode 100644 lib/cologne/formatter/token.js delete mode 100644 lib/cologne/log_utilities.js delete mode 100644 lib/cologne/logger/console.js delete mode 100644 lib/cologne/logger/file.js create mode 100644 lib/formatters/simple.js create mode 100644 lib/formatters/token.js create mode 100644 lib/loggers/console.js create mode 100644 lib/loggers/file.js create mode 100644 lib/utilities.js (limited to 'lib') diff --git a/lib/cologne.js b/lib/cologne.js index 646fe79..392b452 100644 --- a/lib/cologne.js +++ b/lib/cologne.js @@ -1,49 +1,64 @@ 'use strict'; -let LogUtilities = require('./cologne/log_utilities'); +const Utilities = require('./utilities'); + +const internals = { + + // Maps log levels to their syslog labels + + kLevelStrings: [ + 'emerg', + 'alert', + 'crit', + 'error', + 'warning', + 'notice', + 'info', + 'debug' + ], + + // Version of the Cologne Log Format used + + kLogVersion: '2.0.0' +}; /** TYPE DEFINITIONS **/ /** * Main interface for Cologne Loggers * - * @memberof Cologne * @interface ILogger */ /** * Receives any number of cologne log objects and logs them. * - * @memberof Cologne.ILogger + * @memberof ILogger * @function * @name log - * @returns {undefined} */ /** * Main interface for Cologne Formatters * - * @memberof Cologne * @interface IFormatter */ /** * Receives a cologne log object and returns a formatted string. * - * @memberof Cologne.IFormatter + * @memberof IFormatter * @function * @name format - * @param {Cologne.tCologneLog} logObject the log to be formatted + * @param {tCologneLog} logObject the log to be formatted * @returns {string} the formatted log */ /** * The main cologne log format. * - * @memberof Cologne * @typedef {object} tCologneLog - * @property {Number} _timestamp the timestamp in miliseconds with decimal - * numbers representing fractions of miliseconds + * @property {Bigint} _timestamp the timestamp in nanoseconds * @property {String} _cologneLog main identifier, encodes the version of the * cologne log format being used. * @property {String} _from the origin of the log message. @@ -54,32 +69,14 @@ let LogUtilities = require('./cologne/log_utilities'); */ /** - * Cologne is a logger multiplexer that works mainly with a JSON format. It - * can be instantiated with several loggers, or they can be changed after - * the fact. - * - * ## Usage - * - * ``` - * require('cologne'); - * - * let co = new Cologne({ - * from: "Special Worker Logger", - * loggers: [ - * new Cologne.Logger.Console({ - * formatter: new Cologne.Formatter.Token({ - * formatString: '[{{_timestamp}}]{{_from}}: {{message}}' - * }) - * }) - * ] - * }); - * ``` + * The main logger class. It can be instantiated with loggers in order to + * send messages to different destinations. * * @class Cologne */ -let Cologne = class Cologne { +const Cologne = class Cologne { - constructor (config) { + constructor(config) { /** * The name of this logger, useful to distinguish between different @@ -99,12 +96,12 @@ let Cologne = class Cologne { * @name loggers * @instance * @memberof Cologne - * @type Cologne.ILogger[] + * @type ILogger[] * @default [] */ this.loggers = []; - Object.assign(this, config || {}); + Object.assign(this, config); } /** @@ -113,10 +110,10 @@ let Cologne = class Cologne { * @function addLogger * @instance * @memberof Cologne - * @param {Cologne.ILogger} logger the logger to add - * @return {undefined} + * @param {ILogger} logger the logger to add */ - addLogger (logger) { + addLogger(logger) { + this.loggers.push(logger); } @@ -126,55 +123,58 @@ let Cologne = class Cologne { * @function removeLogger * @instance * @memberof Cologne - * @param {Cologne.ILogger} logger the logger to remove - * @return {Cologne.ILogger[]} the removed log, inside an array. + * @param {ILogger} logger the logger to remove + * @return {ILogger[]} the removed log, inside an array. */ - removeLogger (logger) { - let index; + removeLogger(logger) { - index = this.loggers.indexOf(logger); + const index = this.loggers.indexOf(logger); if (index >= 0) { this.loggers.splice(index, 1); } } /** - * Given an item, it builds a cologne log object. this is done - * automatically by the logger, though this is useful if you need - * to attach metadata before logging. + * Given a message, it builds a cologne log object without logging it. + * If you send a cologne log object, it will only update the level. + * + * If the message is an object, the log object will be extended with + * its properties. * * @function buildLog * @instance * @memberof Cologne - * @param {*} item The item to log - * @return {Cologne.tCologneLog} a cologne log object + * @param {*} message The message to log + * @param {number} [level=6] The level of the message to log + * @return {tCologneLog} a cologne log object */ - buildLog (item, level, meta) { - let logObject; + buildLog(rawMessage, level) { + + if (typeof rawMessage === 'undefined' || rawMessage === null || !rawMessage._cologneLog) { - logObject = {}; + const message = typeof rawMessage === 'object' ? Utilities.stringify(rawMessage) : rawMessage; - if (typeof item === 'undefined' || item === null || !item._cologneLog) { - logObject.message = item; - logObject._cologneLog = this.constructor._version; - logObject._from = this.from; - logObject._level = level || 6; - logObject._levelString = this._levelString(logObject._level); - logObject._timestamp = LogUtilities.now(); + const logObject = { + message: String(message), + _cologneLog: internals.kLogVersion, + _from: this.from, + _level: level || 6, + _timestamp: Utilities.now() + }; - if (meta && typeof meta === 'object') { - Object.assign(logObject, meta); + logObject._levelString = internals.kLevelStrings[logObject._level]; + + if (typeof rawMessage === 'object') { + Object.assign(logObject, rawMessage); } return logObject; } - if (item._cologneLog) { - item._level = level || item._level || 6; - item._levelString = this._levelString(item._level); - } + rawMessage._level = level || rawMessage._level; + rawMessage._levelString = internals.kLevelStrings[rawMessage._level]; - return item; + return rawMessage; } /** @@ -184,10 +184,10 @@ let Cologne = class Cologne { * @function log * @instance * @memberof Cologne - * @return {undefined} */ - log () { - this._log.apply(this, [null].concat(Array.prototype.slice.call(arguments))); + log(...logs) { + + this._log(null, ...logs); } /** @@ -196,10 +196,10 @@ let Cologne = class Cologne { * @function debug * @instance * @memberof Cologne - * @return {undefined} */ - debug () { - this._log.apply(this, [7].concat(Array.prototype.slice.call(arguments))); + debug(...logs) { + + this._log(7, ...logs); } /** @@ -208,10 +208,10 @@ let Cologne = class Cologne { * @function info * @instance * @memberof Cologne - * @return {undefined} */ - info () { - this._log.apply(this, [6].concat(Array.prototype.slice.call(arguments))); + info(...logs) { + + this._log(6, ...logs); } /** @@ -220,10 +220,10 @@ let Cologne = class Cologne { * @function notice * @instance * @memberof Cologne - * @return {undefined} */ - notice () { - this._log.apply(this, [5].concat(Array.prototype.slice.call(arguments))); + notice(...logs) { + + this._log(5, ...logs); } /** @@ -232,10 +232,10 @@ let Cologne = class Cologne { * @function warn * @instance * @memberof Cologne - * @return {undefined} */ - warn () { - this._log.apply(this, [4].concat(Array.prototype.slice.call(arguments))); + warn(...logs) { + + this._log(4, ...logs); } /** @@ -244,85 +244,45 @@ let Cologne = class Cologne { * @function error * @instance * @memberof Cologne - * @return {undefined} */ - error () { - this._log.apply(this, [3].concat(Array.prototype.slice.call(arguments))); + error(...logs) { + + this._log(3, ...logs); } // Private method that builds all the logs and sends them to the loggers. - _log (severity) { - let remainingArguments, logObjectArray, log, logger; - - remainingArguments = Array.prototype.slice.call(arguments, 1); - logObjectArray = []; - - for (log of remainingArguments) { - if (typeof log === 'undefined') { - logObjectArray.push(this.buildLog('undefined', severity)); - continue; - } - - if (log === null) { - logObjectArray.push(this.buildLog('null', severity)); - continue; - } - logObjectArray.push(this.buildLog(log, severity)); - } + _log(level, ...logs) { - for (logger of this.loggers) { - logger.log.apply(logger, logObjectArray); - } - } + const structuredLogs = logs.map((log) => this.buildLog(log, level)); - // Private utility method that will return the string for any given - // numerical severity level - _levelString (level) { - switch(level) { - case 0: - return 'emerg'; - case 1: - return 'alert'; - case 2: - return 'crit'; - case 3: - return 'error'; - case 4: - return 'warning'; - case 5: - return 'notice'; - case 6: - return 'info'; - case 7: - return 'debug'; + for (const logger of this.loggers) { + logger.log(...structuredLogs); } } }; -// Version of the Cologne Log Format used. -Cologne._version = '1.0.0'; - /** * Namespace that includes the built-in formatters. * - * @namespace Formatter - * @memberof Cologne + * @namespace Formatters */ -Cologne.Formatter = {}; -Cologne.Formatter.Simple = require('./cologne/formatter/simple'); -Cologne.Formatter.Token = require('./cologne/formatter/token'); +const Formatters = {}; +Formatters.Simple = require('./formatters/simple'); +Formatters.Token = require('./formatters/token'); /** * Namespace that includes the built-in loggers. * - * @namespace Logger - * @memberof Cologne + * @namespace Loggers */ -Cologne.Logger = {}; -Cologne.Logger.Console = require('./cologne/logger/console'); -Cologne.Logger.File = require('./cologne/logger/file'); - -Cologne.LogUtilities = require('./cologne/log_utilities'); - -module.exports = Cologne; +const Loggers = {}; +Loggers.Console = require('./loggers/console'); +Loggers.File = require('./loggers/file'); + +module.exports = { + Cologne, + Formatters, + Loggers, + Utilities +}; diff --git a/lib/cologne/formatter/simple.js b/lib/cologne/formatter/simple.js deleted file mode 100644 index 48821fc..0000000 --- a/lib/cologne/formatter/simple.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -let LogUtilities = require('../log_utilities'); - -/** - * Simple formatter. Outputs a predefined format: - * `[{{_timestamp}}][{{_levelString}}] {{_from}}: {{message}}`; - * - * @memberof Cologne.Formatter - * @implements Cologne.IFormatter - * @class Simple - */ -let SimpleFormatter = class SimpleFormatter { - - constructor (config) { - - /** - * Flag that tells us whether or not to use ANSI color. Defaults to - * false. - * - * @name colorize - * @instance - * @memberof Cologne.Formatter.Simple - * @type Boolean - * @default false - */ - this.colorize = false; - - Object.assign(this, config || {}); - } - - /** - * Main entry point, it will read the incoming log object and convert - * it to the output string. - * - * @function format - * @instance - * @memberof Cologne.Formatter.Simple - * @param {Cologne.tCologneLog} logObjet the log to format - * @return {String} the formatted object - */ - format (logObject) { - let date, levelString; - - date = new Date(logObject._timestamp); - date = date.toISOString(); - levelString = this._colorize(logObject._levelString, logObject._level); - - return `[${date}][${levelString}] ${logObject._from}: ${logObject.message}`; - } - - _colorize (levelString, level) { - let escapeCode, color, reset; - - if (!this.colorize) { - return levelString; - } - - escapeCode = String.fromCharCode(27); - color = escapeCode + LogUtilities.getAnsiCode(LogUtilities.getLevelAnsi(level)); - reset = escapeCode + LogUtilities.getAnsiCode('reset'); - - return color + levelString + reset; - } -}; - -module.exports = SimpleFormatter; diff --git a/lib/cologne/formatter/token.js b/lib/cologne/formatter/token.js deleted file mode 100644 index bb905ca..0000000 --- a/lib/cologne/formatter/token.js +++ /dev/null @@ -1,104 +0,0 @@ -'use strict'; - -let LogUtilities = require('../log_utilities'); - -/** - * Token formatter. Given a format string it will attempt to output - * a message. - * - * @memberof Cologne.Formatter - * @implements Cologne.IFormatter - * @class Token - */ -let TokenFormatter = class TokenFormatter { - constructor (config) { - - /** - * The string to use as a template string. By default, any property - * inside double curly braces `{{likeThis}}` will be extracted from - * the object and replaced. If the object does not contain the - * property, it will leave it. - * - * @name formatString - * @instance - * @memberof Cologne.Formatter.Token - * @type String - * @default '{{message}}' - */ - this.formatString = '{{message}}'; - - /** - * The regex rule to use to match the tokens. - * - * @name replaceRule - * @instance - * @memberof Cologne.Formatter.Token - * @type RegExp - * @default /{{(.*)}}/g - */ - this.replaceRule = /{{(.*?)}}/g; - - /** - * Flag that specifies whether or not to use an isoDate when using - * `_timestamp`. If false it will output the raw timestamp. - * - * @name isoDate - * @instance - * @memberof Cologne.Formatter.Token - * @type Boolean - * @default true - */ - this.isoDate = true; - - this._ansiRe = /_ansi:.+/; - - Object.assign(this, config || {}); - } - - /** - * Main entry point, it will read the incoming log object and convert - * all the tokens to their corresponding representation, finally - * returning the string. - * - * @function format - * @instance - * @memberof Cologne.Formatter.Token - * @param {Cologne.tCologneLog} logObjet the log to format - * @return {String} the formatted object - */ - format (logObject) { - let resultString, escapeCode; - - escapeCode = String.fromCharCode(27); - - resultString = this.formatString.replace(this.replaceRule, function (match, token) { - let date, ansiType; - - if (token === '_timestamp' && this.isoDate) { - date = new Date(logObject._timestamp); - return date.toISOString(); - } - - if (token.match(this._ansiRe)) { - ansiType = token.split(':')[1]; - - // Smartish coloring - if (ansiType === '_level') { - return escapeCode + LogUtilities.getAnsiCode(LogUtilities.getLevelAnsi(logObject._level)); - } - - return escapeCode + LogUtilities.getAnsiCode(ansiType); - } - - if (!logObject.hasOwnProperty(token)) { - return match; - } - - return logObject[token]; - }.bind(this)); - - return resultString; - } -}; - -module.exports = TokenFormatter; diff --git a/lib/cologne/log_utilities.js b/lib/cologne/log_utilities.js deleted file mode 100644 index fc15a8a..0000000 --- a/lib/cologne/log_utilities.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -let microtime = require('microtime'); - -/** - * Container object for utilities used by loggers. - * - * @memberof Cologne - * @class LogUtilities - */ -let LogUtilities = { - - /** - * Returns the current timestamp in miliseconds as a floating point that - * includes fractions (ie. microseconds) - * - * @function now - * @memberof Cologne.LogUtilities - * @return {Number} current time in miliseconds, including fractions. - */ - now: function now() { - return microtime.nowDouble() * 1000; - }, - - /** - * Stringifies objects, avoiding circular references. - * - * @function stringify - * @memberof Cologne.LogUtilities - * @param {Object} object the object to stringify - * @return {String} the stringified object - */ - stringify: function stringify(object) { - let cache; - - cache = []; - - return JSON.stringify(object, function (key, value) { - if (typeof value === 'object' && value !== null) { - if (cache.indexOf(value) !== -1) { - return this._circularString; - } - - cache.push(value); - } - - return value; - }.bind(this)); - }, - - /** - * Given an ansi keyword, it will return the appropriate code. - * - * @function getAnsiCode - * @memberof Cologne.LogUtilities - * @param {String} ansiString the name of the desired code - * @return {String} the ansi code - */ - getAnsiCode: function getAnsiCode(ansiString) { - switch(ansiString) { - case 'bold': - return '[1m'; - case 'italics': - return '[3m'; - case 'underline': - return '[4m'; - case 'inverse': - return '[7m'; - case 'strikethrough': - return '[9m'; - case 'bold_off': - return '[22m'; - case 'italics_off': - return '[23m'; - case 'underline_off': - return '[24m'; - case 'inverse_off': - return '[27m'; - case 'strikethrough_off': - return '[29m'; - case 'black': - return '[30m'; - case 'red': - return '[31m'; - case 'green': - return '[32m'; - case 'yellow': - return '[33m'; - case 'blue': - return '[34m'; - case 'magenta': - return '[35m'; - case 'cyan': - return '[36m'; - case 'white': - return '[37m'; - case 'default': - return '[39m'; - case 'black_bg': - return '[40m'; - case 'red_bg': - return '[41m'; - case 'green_bg': - return '[42m'; - case 'yellow_bg': - return '[43m'; - case 'blue_bg': - return '[44m'; - case 'magenta_bg': - return '[45m'; - case 'cyan_bg': - return '[46m'; - case 'white_bg': - return '[47m'; - case 'default_bg': - return '[49m'; - case 'reset': // for informative purpouses - default: - return '[0m'; - } - }, - - /** - * Given a level, it will return the appropriate ansi keyword related - * to it. - * - * @function getLevelAnsi - * @memberof Cologne.LogUtilities - * @param {number} level the level of the log - * @return {String} the ansi keyword - */ - getLevelAnsi: function getLevelAnsi(level) { - switch(level) { - case 0: - case 1: - case 2: - case 3: - return 'red'; - case 4: - return 'yellow'; - case 5: - case 6: - return 'blue'; - case 7: - return 'green'; - default: - return 'default'; - } - } -}; - -// String used as default circular reference. -LogUtilities._circularString = '[Circular]'; - -module.exports = LogUtilities; diff --git a/lib/cologne/logger/console.js b/lib/cologne/logger/console.js deleted file mode 100644 index 3a73e42..0000000 --- a/lib/cologne/logger/console.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -let LogUtilities = require('../log_utilities'); - -/** - * Logger for the javascript console. - * - * @memberof Cologne.Logger - * @implements Cologne.ILogger - * @class Console - */ -let ConsoleLogger = class ConsoleLogger { - constructor (config) { - - /** - * The console it will write to, can be any object that looks - * and acts like a console, including other cologne objects. - * - * @name console - * @instance - * @memberof Cologne.Logger.Console - * @type Object - * @default global.console - */ - this.console = console; - - /** - * The formatter it will use to output the log. If not present it - * will output raw JSON - * - * @name formatter - * @instance - * @memberof Cologne.Logger.Console - * @type Cologne.IFormatter - * @default null - */ - this.formatter = null; - - Object.assign(this, config || {}); - } - - - /** - * Main entry point, for each incoming argument it will attempt to - * format and send to the console. - * - * @function log - * @instance - * @memberof Cologne.Logger.Console - * @return {undefined} - */ - log () { - let logObject, messages, severity; - - messages = []; - - for (logObject of arguments) { - messages.push(this._format(logObject)); - - if (!severity) { - severity = logObject._level; - } - } - - switch(severity) { - case 0: - case 1: - case 2: - case 3: - this.console.error.apply(this.console, messages); - break; - case 4: - this.console.warn.apply(this.console, messages); - break; - case 5: - case 6: - this.console.info.apply(this.console, messages); - break; - case 7: - default: - this.console.log.apply(this.console, messages); - break; - } - } - - _format (logObject) { - if (this.formatter) { - return this.formatter.format(logObject); - } - - return LogUtilities.stringify(logObject); - } -}; - -module.exports = ConsoleLogger; diff --git a/lib/cologne/logger/file.js b/lib/cologne/logger/file.js deleted file mode 100644 index e9a56e7..0000000 --- a/lib/cologne/logger/file.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -let fs = require('fs'); - -let LogUtilities = require('../log_utilities'); - -/** - * Logger for files. - * - * @memberof Cologne.Logger - * @implements Cologne.ILogger - * @class File - */ -let FileLogger = class FileLogger { - constructor (config) { - - /** - * Path to the file it will write to, must be readable. - * - * @name file - * @instance - * @memberof Cologne.Logger.File - * @type string - * @default null - */ - this.file = null; - - /** - * The formatter it will use to output the log. If not present it - * will output raw JSON - * - * @name formatter - * @instance - * @memberof Cologne.Logger.File - * @type Cologne.IFormatter - * @default null - */ - this.formatter = null; - - Object.assign(this, config || {}); - - this._stream = fs.createWriteStream(this.file, {flags: 'a'}); - } - - /** - * Main entry point, for each incoming argument it will attempt to - * format and send to the stream to be written. - * - * @function log - * @instance - * @memberof Cologne.Logger.File - * @return {undefined} - */ - log () { - let logObject; - - for (logObject of arguments) { - this._stream.write(this.format(logObject) + '\n'); - } - } - - format (logObject) { - if (this.formatter) { - return this.formatter.format(logObject); - } - - return LogUtilities.stringify(logObject); - } -}; - -module.exports = FileLogger; diff --git a/lib/formatters/simple.js b/lib/formatters/simple.js new file mode 100644 index 0000000..bfaa76c --- /dev/null +++ b/lib/formatters/simple.js @@ -0,0 +1,62 @@ +'use strict'; + +const Utilities = require('../utilities'); + +/** + * Simple formatter. Outputs a predefined format: + * `[{{_timestamp}}][{{_levelString}}] {{_from}}: {{message}}`; + * + * @memberof Formatters + * @implements IFormatter + * @class Simple + */ +module.exports = class SimpleFormatter { + + constructor(config) { + + /** + * Flag that tells us whether or not to use ANSI color. Defaults to + * false. + * + * @name colorize + * @instance + * @memberof Formatters.Simple + * @type Boolean + * @default false + */ + this.colorize = false; + + Object.assign(this, config || {}); + } + + /** + * Main entry point, it will read the incoming log object and convert + * it to the output string. + * + * @function format + * @instance + * @memberof Formatters.Simple + * @param {tCologneLog} logObjet the log to format + * @return {String} the formatted object + */ + format(logObject) { + + const date = (new Date(Number(logObject._timestamp) / 1000000)).toISOString(); + const levelString = this._colorize(logObject._levelString, logObject._level); + + return `[${date}][${levelString}] ${logObject._from}: ${logObject.message}`; + } + + _colorize(levelString, level) { + + if (!this.colorize) { + return levelString; + } + + const escapeCode = String.fromCharCode(27); + const color = escapeCode + Utilities.getAnsiCode(Utilities.getLevelAnsi(level)); + const reset = escapeCode + Utilities.getAnsiCode('reset'); + + return color + levelString + reset; + } +}; diff --git a/lib/formatters/token.js b/lib/formatters/token.js new file mode 100644 index 0000000..2ec3a17 --- /dev/null +++ b/lib/formatters/token.js @@ -0,0 +1,93 @@ +'use strict'; + +const Utilities = require('../utilities'); + +/** + * Token formatter. Given a format string it will attempt to output + * a message. + * + * @memberof Formatters + * @implements IFormatter + * @class Token + */ +module.exports = class TokenFormatter { + constructor(config) { + + /** + * The string to use as a template string. By default, any property + * inside double curly braces `{{likeThis}}` will be extracted from + * the object and replaced. If the object does not contain the + * property, it will leave it. + * + * @name formatString + * @instance + * @memberof Formatters.Token + * @type String + * @default '{{message}}' + */ + this.formatString = '{{message}}'; + + /** + * The regex rule to use to match the tokens. + * + * @name replaceRule + * @instance + * @memberof Formatters.Token + * @type RegExp + * @default /{{(.*)}}/g + */ + this.replaceRule = /{{(.*?)}}/g; + + /** + * Flag that specifies whether or not to use an isoDate when using + * `_timestamp`. If false it will output the raw timestamp. + * + * @name isoDate + * @instance + * @memberof Formatters.Token + * @type Boolean + * @default true + */ + this.isoDate = true; + + this._ansiRe = /_ansi:.+/; + + Object.assign(this, config || {}); + } + + /** + * Main entry point, it will read the incoming log object and convert + * all the tokens to their corresponding representation, finally + * returning the string. + * + * @function format + * @instance + * @memberof Formatters.Token + * @param {tCologneLog} log the log to format + * @return {String} the formatted object + */ + format(log) { + + const escapeCode = String.fromCharCode(27); + return this.formatString.replace(this.replaceRule, (match, token) => { + + if (token === '_timestamp' && this.isoDate) { + const date = new Date(Number(log._timestamp) / 1000000); + return date.toISOString(); + } + + if (token.match(this._ansiRe)) { + const ansiType = token.split(':')[1]; + + // Smartish coloring + if (ansiType === '_level') { + return escapeCode + Utilities.getAnsiCode(Utilities.getLevelAnsi(log._level)); + } + + return escapeCode + Utilities.getAnsiCode(ansiType); + } + + return log[token] || match; + }); + } +}; diff --git a/lib/loggers/console.js b/lib/loggers/console.js new file mode 100644 index 0000000..116b04b --- /dev/null +++ b/lib/loggers/console.js @@ -0,0 +1,94 @@ +'use strict'; + +const Utilities = require('../utilities'); + +/** + * Logger for the javascript console. + * + * @implements ILogger + * @class Console + */ +module.exports = class ConsoleLogger { + constructor(config) { + + /** + * The console it will write to, can be any object that looks + * and acts like a console, including other cologne objects. + * + * @name console + * @instance + * @memberof Loggers.Console + * @type Object + * @default global.console + */ + this.console = console; + + /** + * The formatter it will use to output the log. If not present it + * will output raw JSON + * + * @name formatter + * @instance + * @memberof Loggers.Console + * @type IFormatter + * @default null + */ + this.formatter = null; + + Object.assign(this, config); + } + + + /** + * Main entry point, for each incoming argument it will attempt to + * format and send to the console. + * + * @function log + * @instance + * @memberof Loggers.Console + * @return {undefined} + */ + log(...logs) { + + const formattedLogs = logs.map((log) => ({ log: this._format(log), level: log._level })); + + for (const { log, level } of formattedLogs) { + this._log(log, level ); + } + + } + + // Routes an individual log to the appropriatet console + + _log(log, level) { + + switch (level) { + case 0: + case 1: + case 2: + case 3: + this.console.error(log); + break; + case 4: + this.console.warn(log); + break; + case 5: + case 6: + this.console.info(log); + break; + case 7: + default: + this.console.log(log); + break; + } + } + + _format(logObject) { + + if (this.formatter) { + return this.formatter.format(logObject); + } + + return Utilities.stringify(logObject); + } +}; diff --git a/lib/loggers/file.js b/lib/loggers/file.js new file mode 100644 index 0000000..50a251e --- /dev/null +++ b/lib/loggers/file.js @@ -0,0 +1,68 @@ +'use strict'; + +const Fs = require('fs'); +const Utilities = require('../utilities'); + +/** + * Logger for files. + * + * @memberof Loggers + * @implements ILogger + * @class File + */ +module.exports = class FileLogger { + constructor(config) { + + /** + * Path to the file it will write to, must be readable. + * + * @name file + * @instance + * @memberof Loggers.File + * @type string + * @default null + */ + this.file = null; + + /** + * The formatter it will use to output the log. If not present it + * will output raw JSON + * + * @name formatter + * @instance + * @memberof Loggers.File + * @type IFormatter + * @default null + */ + this.formatter = null; + + Object.assign(this, config); + + this._stream = Fs.createWriteStream(this.file, { flags: 'a' }); + } + + /** + * Main entry point, for each incoming argument it will attempt to + * format and send to the stream to be written. + * + * @function log + * @instance + * @memberof Loggers.File + * @return {undefined} + */ + log(...logs) { + + for (const log of logs) { + this._stream.write(this._format(log) + '\n'); + } + } + + _format(logObject) { + + if (this.formatter) { + return this.formatter.format(logObject); + } + + return Utilities.stringify(logObject); + } +}; diff --git a/lib/utilities.js b/lib/utilities.js new file mode 100644 index 0000000..1c930af --- /dev/null +++ b/lib/utilities.js @@ -0,0 +1,169 @@ +'use strict'; + +const internals = { + kCircularString: '[Circular]', + + // Get High Res time depending on platform. Currently only supporting node. + + getHighResTime() { + + return process.hrtime.bigint(); + } +}; + +// Initialise relative time. + +internals.initialTimestamp = BigInt(Date.now()) * 1000000n; +internals.initialHighResTime = internals.getHighResTime(); + +/** + * Container object for utilities used by loggers. + * + * @class Utilities + */ +module.exports = { + + /** + * Returns the current timestamp in nanoseconds as a bigint. + * + * @function now + * @memberof Utilities + * @return {bigint} current time in nanoseconds, including fractions. + */ + now() { + + return internals.initialTimestamp + internals.getHighResTime() - internals.initialHighResTime; + }, + + /** + * Stringifies objects, avoiding circular references. + * + * @function stringify + * @memberof Utilities + * @param {Object} object the object to stringify + * @return {String} the stringified object + */ + stringify(object) { + + const cache = new Set(); + + return JSON.stringify(object, (key, value) => { + + if (typeof value === 'bigint') { + return String(value) + 'n'; + } + + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + return internals.kCircularString; + } + + cache.add(value); + } + + return value; + }); + }, + + /** + * Given an ansi keyword, it will return the appropriate code. + * + * @function getAnsiCode + * @memberof Utilities + * @param {String} ansiString the name of the desired code + * @return {String} the ansi code + */ + getAnsiCode(ansiString) { + + switch (ansiString) { + case 'bold': + return '[1m'; + case 'italics': + return '[3m'; + case 'underline': + return '[4m'; + case 'inverse': + return '[7m'; + case 'strikethrough': + return '[9m'; + case 'bold_off': + return '[22m'; + case 'italics_off': + return '[23m'; + case 'underline_off': + return '[24m'; + case 'inverse_off': + return '[27m'; + case 'strikethrough_off': + return '[29m'; + case 'black': + return '[30m'; + case 'red': + return '[31m'; + case 'green': + return '[32m'; + case 'yellow': + return '[33m'; + case 'blue': + return '[34m'; + case 'magenta': + return '[35m'; + case 'cyan': + return '[36m'; + case 'white': + return '[37m'; + case 'default': + return '[39m'; + case 'black_bg': + return '[40m'; + case 'red_bg': + return '[41m'; + case 'green_bg': + return '[42m'; + case 'yellow_bg': + return '[43m'; + case 'blue_bg': + return '[44m'; + case 'magenta_bg': + return '[45m'; + case 'cyan_bg': + return '[46m'; + case 'white_bg': + return '[47m'; + case 'default_bg': + return '[49m'; + case 'reset': // for informative purpouses + default: + return '[0m'; + } + }, + + /** + * Given a level, it will return the appropriate ansi keyword related + * to it. + * + * @function getLevelAnsi + * @memberof Utilities + * @param {number} level the level of the log + * @return {String} the ansi keyword + */ + getLevelAnsi(level) { + + switch (level) { + case 0: + case 1: + case 2: + case 3: + return 'red'; + case 4: + return 'yellow'; + case 5: + case 6: + return 'blue'; + case 7: + return 'green'; + default: + return 'default'; + } + } +}; -- cgit