'use strict';
class Logger {
/** @summary <h1>JavaScript logger singleton to handle errors the same way</h1>
* @author Arthur Beaulieu
* @since June 2020
* @description <blockquote>The Logger class provides a singleton object to allow brain dead logging for frontend
* JavaScript code. Errors can be raised from JavaScript errors (<code>new Error()</code>), or using a custom error
* format, with a severity, title and message. It is also possible to pass a notification manager object to handle
* those error either in console and in UI. The recommended manager to use for notification can be found at
* <a href="https://github.com/ArthurBeaulieu/Notification.js" alt="notification-js">Notification.js</a>. You can
* otherwise implement you system, but it as to take a type (severity), a title and a message ; for further information,
* refer to the <code>_logErrorToNotification</code> documentation. For source code, please go to
* <a href="https://github.com/ArthurBeaulieu/Logger.js" alt="logger-js">Logger.js</a></blockquote>
* @param {object} [options={}] - The Logger object, not mandatory but it is recommended to provide one for full features
* @param {object} [options.errors={}] - The custom errors, JSON style, with key being the error name and value being
* an object with a <code>severity</code>, a <code>title</code> and a <code>message</code> property (all strings)
* @param {object} [options.notification=null] - The notification manager (to create new notifications when logging)
* @param {boolean} [options.log=true] - Allow console logging (turn to false in prod environment)
* @return {object} - The Logger singleton instance */
constructor(options = {}) {
// If an instance of Logger already exists, we just return it
if (!!Logger.instance) {
return Logger.instance;
}
// Set object instance
Logger.instance = this;
// Prevent wrong type for arguments, fallback according to attribute utility
if (typeof options.errors !== 'object') {
options.errors = {}; // Needs to define to empty object to avoid errors when checking custom errors
}
if (typeof options.notification !== 'object') {
options.notification = null; // Null to ignore the notification step in error raising
}
if (typeof options.log !== 'boolean') {
options.log = true; // No log means... useful component right?
}
/** @private
* @member {object} - The error messages to use in Logger */
this._errors = options.errors;
/** @private
* @member {object} - The custom notification handler, must be able to take type, title and message (at least) */
this._notification = options.notification;
/** @private
* @member {boolean} - Internal logging flag from constructor options, allow to output each event action */
this._log = options.log;
/** @public
* @member {string} - Component version */
this.version = '1.2.0';
return this;
}
/** @method
* @name destroy
* @public
* @memberof Logger
* @description <blockquote>Logger destructor. Will delete singleton instance and its properties.</blockquote> */
destroy() {
// Delete object attributes
Object.keys(this).forEach(key => {
delete this[key];
});
// Clear singleton instance
Logger.instance = null;
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ---------------------------------------- LOGGER JS INTERN METHODS ------------------------------------------ */
/* */
/* These internal methods will build a raised error depending on logging level sent when building this singleton. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name _buildErrorInfo
* @private
* @memberof Logger
* @description <blockquote>This method will be the error properties according to its type. A custom error will
* take values defined at construction of this singleton. JavaScrip error are parsed to extract title and
* message properties from stack, with specific handling for Chrome and Firefox.</blockquote>
* @param {object} error - The error to build info from. Can be a custom error or a standard JavaScript error
* @return {object} - The error properties ; <code>severity</code>, <code>title</code> and <code>message</code> */
_buildErrorInfo(error) {
let severity = '';
let title = '';
let message = '';
if (typeof error === 'object' || typeof error === 'string') {
// this._errors doesn't contain the error key ; either a Js error or an unknown error
if (this._errors[error] === undefined) {
// JavaScript error created with new Error(), that need to contain fileName, message, line and column number
let filename = '';
if (error.fileName && error.message && error.lineNumber && error.columnNumber) { // Firefox specific
filename = error.fileName.match(/\/([^\/]+)\/?$/)[1];
severity = 'error';
title = `JavaScript error`;
message = `${error.message} in file ${filename}:${error.lineNumber}:${error.columnNumber}`;
} else if (error.message && error.stack) { // Chrome specific
filename = error.stack.split('\n')[error.stack.split('\n').length - 1].match(/\/([^\/]+)\/?$/)[1];
severity = 'error';
title = `JavaScript error`;
message = `${error.message} in file ${filename}`;
} else { // Unknown error that do not require any arguments
severity = 'error';
title = `Unexpected error ${error}`;
message = 'The error object sent to Logger.raise() is neither a JavaScript error nor a custom error (with severity, title and message).';
}
} else { // Custom error that need to be filled with a severity, a title and a message
severity = this._errors[error].severity || '';
title = this._errors[error].title || '';
message = this._errors[error].message || '';
}
}
// Return error standard properties
return {
severity: severity,
title: title,
message: message
};
}
/** @method
* @name _logErrorToNotification
* @private
* @memberof Logger
* @description <blockquote>This method will call for a new notification if a component has been given to this singleton
* constructor. The component must expose a <code>new()</code> methods that takes as arguments the Logger standard properties ;
* <code>severity</code>, <code>title</code> and <code>message</code>. If no component has be provided, this method won't do anything.
* One can find such component <a href="https://github.com/ArthurBeaulieu/Notification.js" alt="notification-js">here</a>.</blockquote>
* @param {object} errorParameters - The error with Logger standard properties (<code>severity</code>, <code>title</code> and <code>message</code>) */
_logErrorToNotification(errorParameters) {
if (this._notification && typeof errorParameters === 'object') {
this._notification.new({
type: errorParameters.severity || 'error',
title: errorParameters.title || 'Can\'t get error info',
message: errorParameters.message || 'Call for new notification wasn\'t made with arguments'
});
}
}
/** @method
* @name _logErrorToConsole
* @private
* @memberof Logger
* @description <blockquote>This method will send error to console if logging has been allowed to this singleton constructor.
* It takes a Logger standard error (<code>severity</code>, <code>title</code> and <code>message</code>) as argument.
* It will build a unified output regardless the Chrome or Firefox browser. It enhance <code>console.log</code> and
* <code>console.info</code> to also display the stack trace in a <code>console.group</code>.</blockquote>
* @param {object} errorParameters - The error with Logger standard properties (<code>severity</code>, <code>title</code> and <code>message</code>) */
_logErrorToConsole(errorParameters) {
if (this._log && typeof errorParameters === 'object') {
// Missing mandatory arguments
if (!errorParameters.severity && !errorParameters.title && !errorParameters.message) {
return;
}
/* Colors to use, extracted from Notification.js (https://github.com/ArthurBeaulieu/Notification.js) */
const colors = {
success: 'color: rgb(76, 175, 80);',
info: 'color: rgb(3, 169, 244);',
warning: 'color: rgb(255, 152, 0);',
error: 'color: rgb(244, 67, 54);'
};
const browsers = {
firefox: /firefox/i.test(navigator.userAgent),
chrome: /chrome/i.test(navigator.userAgent) && /google inc/i.test(navigator.vendor)
};
// Compute log level from severity, and handle warn and log as warning and success
let logLevel = errorParameters.severity;
if (errorParameters.severity === 'warning') {
logLevel = 'warn';
} else if (errorParameters.severity === 'success') {
logLevel = 'log';
}
// Create console group with associated style
console.groupCollapsed(`%c${errorParameters.severity.toUpperCase()}: ${errorParameters.title}`, colors[errorParameters.severity]);
// Apply type and severity to build console call
const outputString = `%c${errorParameters.message}\n${this._getCallerName(browsers)}`;
console[logLevel](outputString, colors[errorParameters.severity]);
// Only append console trace if severity is not an error (as error already display trace)
if (errorParameters.severity !== 'error' && errorParameters.severity !== 'warning') {
console.trace();
}
// Close error group in console
console.groupEnd();
}
}
/** @method
* @name _getCallerName
* @private
* @memberof Logger
* @description <blockquote>This method will build the caller name as a string, formatted to be easy to
* read and display in the log output.</blockquote>
* @param {object} browsers - An object with booleans values for current browser used by session
* @return {string} - The Logger standard caller name regardless the browser */
_getCallerName(browsers) {
// Original code from https://gist.github.com/irisli/716b6dacd3f151ce2b7e
let caller = (new Error()).stack; // Create error and get its call stack
// Get last called depending on browser
if (typeof browsers === 'object') {
if (browsers.firefox) {
caller = caller.split('\n')[3]; // Third item is error caller method
caller = caller.replace(/@+/, ' '); // Change `@` to `(`
} else if (browsers.chrome) {
caller = caller.split('\n')[caller.split('\n').length - 2]; // Minus 2 to remove closing parenthesis as well
// Remove Chrome specific strings to match Firefox look and feel (go ff)
caller = caller.replace(/^Error\s+/, '');
caller = caller.replace(/^\s+at./, '');
caller = caller.replace(/[{()}]/g, '');
} else {
return 'Unsupported browser to get the caller name from';
}
} else {
return 'Argument error, unable to get the caller name on this raise';
}
// Prepare function name, and replace with anonymous in proper case
let functionName = caller;
if (caller.charAt(0) === ' ') { // First char is normally the function name first char. Space means anonymous cross browsers (so far...)
functionName = `<anonymous>${caller}`;
}
// Unified returned value for anonymous/non anonymous methods
return `Raised from function ${functionName}`;
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ---------------------------------------- LOGGER JS PUBLIC METHOD ------------------------------------------- */
/* */
/* These are the exposed method of Logger component. It allows to raise error that will be displayed in the */
/* console if needed, and displayed in the interface using a notification component. Otherwise, it won't do */
/* anything. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name raise
* @public
* @memberof Logger
* @description <blockquote>The raise method will build, according to argument sent to this singleton constructor,
* a console output and/or a notification for the given error. The input error can be a standard JavaScript error,
* raised like <code>new Error()</code>, but can also be build using the custom format, using the key of the error
* as input string. See constructor and example for demonstration.</blockquote>
* @param {object} error - The error to handle. Can be a custom error or a standard JavaScript error */
raise(error) {
// Create error specific values depending on error origin (JavaScript, Custom or Unknown) */
const errorParameters = this._buildErrorInfo(error);
/* If any Notification manager exists, use it with error parameters */
this._logErrorToNotification(errorParameters);
/* In debug mode, fill the console with error parameters */
this._logErrorToConsole(errorParameters);
}
}
export default Logger;