utils/logger.js

/*
 * Your installation or use of this SugarCRM file is subject to the applicable
 * terms available at
 * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
 * If you do not agree to all of the applicable terms or do not have the
 * authority to bind the entity as an authorized representative, then do not
 * install or use this SugarCRM file.
 *
 * Copyright (C) SugarCRM Inc. All rights reserved.
 */

/**
 * Application logger.
 *
 * Usage example:
 *
 * ```
 * const logger = require('./logger')({
 *     level: 'DEBUG',
 * });
 *
 * // Log a string message
 * logger.debug('Some debug message');
 *
 * // Log an object
 * let obj = { foo: 'bar' };
 * logger.info(obj);
 *
 * // Log a closure.
 * let a = 1;
 * logger.error(function() { return a; });
 * ```
 *
 * @module Core/Logger
 */

let config =  {
    level: 'error',
    formatter: 'SimpleFormatter',
    consoleWriter: 'ConsoleWriter',
    serverWriter: 'ServerWriter',
};

const writers = {
    /**
     * Default client-side log writer.
     * Outputs messages onto browser's console object.
     *
     * @private
     */
    ConsoleWriter: {
        /**
         * Writes a message with the given log level to the browser console.
         * The writer uses:
         *
         *  - `console.info`: `TRACE`, `DEBUG` and `INFO`.
         *  - `console.warn`: `WARN`.
         *  - `console.error`: `ERROR` and `FATAL`.
         *
         * @param {Utils.Logger.levels} level A logger level from logger.levels
         * @param {string} message The message to write.
         * @method
         */
         write: function(level, message) {
            // work around for browsers without console
            if (!window.console) window.console = {};
            if (!window.console.log) window.console.log = function() { };
            if (level.value <= Logger.levels.INFO.value) {
                console.log(message);
            }
            else if (level.value == Logger.levels.WARN.value) {
                console.warn(message);
            }
            else {
                console.error(message);
            }
        },
    },

    /**
     * Default server-side log writer.
     * Writes log messages to the server.
     *
     * @private
     */
    ServerWriter: {
        /**
         * Writes a log message with a given {@link Utils.Logger.levels}
         * level onto the server.
         *
         * @param {Utils.Logger.levels} level Logger level.
         * @param {string} message Log message.
         * @throws {string} Exceptions are thrown whether the returned
         *   status is `false` or an error occurred while processing the
         *   request.
         */
        write: function(level, message) {
            // FIXME: add support for other logger levels (SC-5483).
            if (level.value < Logger.levels.WARN.value) {
                return;
            }

            if (!SUGAR.App.api.isAuthenticated()) {
                return;
            }

            let write = config.writeToServer || false;
            if (!write || !message.trim().length) {
                return;
            }

            let url = SUGAR.App.api.buildURL(undefined, 'logger');
            let params = {
                level: level.name.toLowerCase(),
                message: message,
            };

            SUGAR.App.api.call('create', url, params, {
                success: function(data) {
                    if (!data.status) {
                        throw 'Failed to write log message {' + message + '} onto server';
                    }
                },
                error: function(e) {
                    throw e;
                }
            });
        },
    },
};

const formatters = {
    /**
     * Default formatter for log messages.
     * Formats a log message as a string with log level and UTC timestamp.
     * ```
     * const Logger = require('./logger');
     *
     * // Log a trace message
     * Logger.trace('Blah-blah');
     *
     * // Output
     * // TRACE[2012-1-26 2:38:23]: Blah-blah
     * ```
     *
     * @private
     */
    SimpleFormatter: {
        /**
         * Formats a log message by adding log level name and UTC timestamp.
         *
         * @param {Object} level Logging level.
         * @param {string} message The message to log.
         * @param {Date} date Logging timestamp.
         * @return {string} The formatted log message.
         */
        format: function(level, message, date) {
            let dateString = date.getUTCFullYear() + '-' + (date.getUTCMonth() + 1) + '-' + date.getUTCDate() +
                ' ' + date.getUTCHours() + ':' + date.getUTCMinutes() + ':' + date.getUTCSeconds();
            return level.name + '[' + dateString + ']: ' + message;
        },
    },
};

/**
 * @alias module:Core/Logger
 */
const Logger = {
    /**
     * Logging levels.
     *
     * @private
     * @type {Object}
     */
    levels: {
        /**
         * Trace log level
         */
        TRACE: {
            value: 1,
            name: 'TRACE',
        },
        /**
         * Debug log level
         */
        DEBUG: {
            value: 2,
            name: 'DEBUG',
        },
        /**
         * Info log level
         */
        INFO: {
            value: 3,
            name: 'INFO',
        },
        /**
         * Warn log level
         */
        WARN: {
            value: 4,
            name: 'WARN',
        },
        /**
         * Error log level
         */
        ERROR: {
            value: 5,
            name: 'ERROR',
        },
        /**
         * Fatal log level
         */
        FATAL: {
            value: 6,
            name: 'FATAL',
        }
    },

    /**
     * Logs a message with the TRACE log level.
     *
     * @method
     * @param {string|Object|Function} message Message to log.
     */
    trace: function(message) {
        this.log(this.levels.TRACE, message);
    },

    /**
     * Logs a message with the DEBUG log level.
     *
     * @param {string|Object|Function} message Message to log.
     */
    debug: function(message) {
        this.log(this.levels.DEBUG, message);
    },

    /**
     * Logs a message with the INFO log level.
     *
     * @param {string|Object|Function} message Message to log.
     */
    info: function(message) {
        this.log(this.levels.INFO, message);
    },

    /**
     * Logs a message with the WARN log level.
     *
     * @param {string|Object|Function} message Message to log.
     */
    warn: function(message) {
        this.log(this.levels.WARN, message);
    },

    /**
     * Logs a message with the ERROR log level.
     *
     * @param {string|Object|Function} message Message to log.
     */
    error: function(message) {
        this.log(this.levels.ERROR, message);
    },

    /**
     * Logs a message with the FATAL log level.
     *
     * @param {string|Object|Function} message Message to log.
     */
    fatal: function(message) {
        this.log(this.levels.FATAL, message);
    },

    /**
     * Retrieves logger level based on system settings.
     *
     * @return {Object} Logger level or `ERROR` if none defined.
     */
    getLevel: function() {
        let level = config.level;

        if (!level) {
            return this.levels.ERROR;
        }

        level = level.toUpperCase();

        // FIXME this needs to be done after SC-5483 is implemented
        /*
        if (!this.levels[level]) {

            console.error('Your logger level is set to an invalid value. ' +
                'Please redefine it in Administration > System Settings. ' +
                'If you continue to see this warning, please ' +
                'contact your Admin.');
        }
        */

        return this.levels[level] || this.levels.ERROR;
    },

    /**
     * Logs a message with a given {@link Utils.Logger.levels} level.
     * If the message is an object, it will be serialized into a JSON string.
     * If the message is a function, it will evaluated in the logger's scope.
     *
     * @param {Utils.Logger.levels} level log level
     * @param {string|Object|Function} message log message
     */
    log: function(level, message) {
        try {
            let currentLevel = this.getLevel();
            if (level.value < currentLevel.value) {
                return;
            }

            message = message || '<none>';

            if (_.isFunction(message)) {
                message = message.call(this);
            }

            if (_.isObject(message)) {
                // Try to json-ify the object. It'll fail if it has circular
                // dependency
                try {
                    message = JSON.stringify(message);
                } catch (e) {
                    message = message.toString();
                }
            }

            const formatter = formatters[config.formatter] || formatters.SimpleFormatter;
            const consoleWriter = writers[config.consoleWriter] || writers.ConsoleWriter;
            const serverWriter = writers[config.serverWriter] || writers.ServerWriter;

            message = formatter.format(level, message, new Date());

            consoleWriter.write(level, message);
            serverWriter.write(level, message);
        } catch (e) {
            console.log('Failed to log message {' + message + '} due to exception: ' + e);
        }
    }
};

Object.defineProperty(Logger, 'SimpleFormatter', {
    get: function() {
        SUGAR.App.logger.warn('`Utils.Logger.SimpleFormatter` has been made private since 7.10 and you will not be able ' +
            'to access it in the next release. Please do not use it anymore');
        return formatters.SimpleFormatter;
    }
});

Object.defineProperty(Logger, 'ServerWriter', {
    get: function() {
        SUGAR.App.logger.warn('`Utils.Logger.ServerWriter` has been made private since 7.10 and you will not be able ' +
            'to access it in the next release. Please do not use it anymore');
        return writers.ServerWriter;
    }
});

Object.defineProperty(Logger, 'ConsoleWriter', {
    get: function() {
        SUGAR.App.logger.warn('`Utils.Logger.ConsoleWriter` has been made private since 7.10 and you will not be able ' +
            'to access it in the next release. Please do not use it anymore');
        return writers.ConsoleWriter;
    }
});

module.exports = function(options) {
    // Config should be immutable. So we should do :
    // config = _.extend(config, {default config}, options);
    // For now, to keep backward compatibility with 7.9, we do the following:
    config = options || {};
    return Logger;
}