Fannon/semlog

View on GitHub
index.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * semlog
 * A semantic logger that colors and formats messages automatically according to the content
 *
 * @author Simon Heimler
 */

'use strict';

//////////////////////////////////////////
// Requirements                         //
//////////////////////////////////////////

var chalk = require('chalk');
var prettyjson = require('prettyjson');

//////////////////////////////////////////
// Variables                            //
//////////////////////////////////////////

if (!global.githubFannonSemlog) {
    global.githubFannonSemlog = {
        history: [],
        config: {
            silent: false,
            colorize: true,
            printYaml: false,
            logDateTime: true,
            printTime: true,
            printDateTime: false,
            printDebug: true,
            printVerbose: true,
            historySize: 2048 // 0 for none
        },
        statistics: {
            debug: 0,
            warning: 0,
            error: 0,
            total: 0
        }
    };
}

/** Publicly export the chalk color library */
exports.chalk = chalk;

//////////////////////////////////////////
// LOGGING FUNCTIONS                    //
//////////////////////////////////////////

/**
 * Custom Logging function
 *
 * Writes Logs to console, stringifies objects first
 *
 * @param {string|object}   msg     Message String or Object
 * @param {boolean}         [silent]  Dot not print message to the console, but stores it to the log history.
 */
exports.log = function(obj, silent) {
    if (obj && obj instanceof Error) {
        exports.error(obj, silent);
    } else if (obj && typeof obj === 'object') {
        exports.debug(obj, silent);
    } else {
        exports.message(obj, silent);
    }
};

exports.message = function(msg, silent) {

    global.githubFannonSemlog.statistics.total += 1;
    if (msg && msg.indexOf && (msg.indexOf('[V]') >= 0 || msg.indexOf('[D]') >= 0)) {
        global.githubFannonSemlog.statistics.debug += 1;
    } else if (msg && msg.indexOf && msg.indexOf('[W]') >= 0) {
        global.githubFannonSemlog.statistics.warning += 1;
    } else if (msg && msg.indexOf && msg.indexOf('[E]') >= 0) {
        global.githubFannonSemlog.statistics.error += 1;
    }

    var config = global.githubFannonSemlog.config;
    silent = silent || config.silent;

    if (typeof msg !== 'string') {
        try {
            msg = '' + JSON.stringify(msg);
        } catch (e) {
            msg = '[E] [SEMLOG] Could not stringify given parameter';
        }
    }

    exports.addToHistory(msg);

    if (!silent) {

        if (config.colorize) {
            msg = exports.colorize(msg);
        }

        if ((config.printTime || config.printDateTime) && msg.trim && msg.trim().length > 0) {
            if (config.printDateTime) {
                msg = chalk.gray('[' + exports.humanDate() + '] ') + msg;
            } else {
                msg = chalk.gray('[' + exports.humanTime() + '] ') + msg;
            }
        }

        if (!config.printVerbose && msg.indexOf('[V]') >= 0) {
            // Supressing output of verbose message
        } else if (!config.printDebug && msg.indexOf('[D]') >= 0) {
            // Supressing output of debug message
        } else {
            console.log(msg);
        }
    }
};

/**
 * Prints out debugging information for the current model object
 *
 * @param {object}        obj     Object
 */
exports.debug = function(obj, silent) {

    global.githubFannonSemlog.statistics.total += 1;

    var config = global.githubFannonSemlog.config;
    silent = silent || config.silent;
    exports.addToHistory(obj);

    if (!silent) {
        if (global.githubFannonSemlog.config.printYaml) {
            // Print YAML
            var options = {
                keysColor: 'white',
                dashColor: 'white',
                stringColor: 'yellow',
                numberColor: 'blue'
            };

            if (!global.githubFannonSemlog.config.colorize) {
                options.noColor = true;
            }
            console.log(chalk.gray('---\n') + prettyjson.render(obj, options));

        } else {
            // Print indented JSON
            var msg = JSON.stringify(obj, false, 4);
            console.log(chalk.gray(msg));
        }
    }
};

/**
 * Prints errors
 *
 * @param {object}        obj     Object
 */
exports.error = function(obj, silent) {

    global.githubFannonSemlog.statistics.total += 1;
    global.githubFannonSemlog.statistics.error += 1;

    var config = global.githubFannonSemlog.config;
    silent = silent || config.silent;
    exports.addToHistory(obj);

    if (!silent) {
        console.error(chalk.red('[E] ' + obj.message));
        console.log(chalk.gray(JSON.stringify(obj, null, 4)));
        if (obj.stack) {
            console.log(chalk.gray(obj.stack));
        }
    }
};


exports.addToHistory = function(obj) {

    var config = global.githubFannonSemlog.config;
    var msg = '';

    try {
        msg = JSON.stringify(obj, null, 4);

        // Check that the message history size doesn't get to big
        if (config.historySize && global.githubFannonSemlog.history.length >= config.historySize) {
            global.githubFannonSemlog.history.shift();
        }

        if (config.logDateTime) {
            msg = '[' + exports.humanDate() + '] ' + msg;
        }

    } catch (e) {
        msg = '[W] Internal semlog error: Could not push circular/invalid object into the history';
    }

    global.githubFannonSemlog.history.push(msg);
};

/**
 * Colors the messages by searching for specific indicator strings
 * TODO: Allow to add to the colorMap
 *
 * @param {string} msg
 * @returns {string}
 */
exports.colorize = function(msg) {

    var colorMap = {
        '[E]': 'red',         // ERROR
        '[W]': 'yellow',      // WARNING
        '[?]': 'yellow',      // MISSING
        '[S]': 'green',       // SUCCESS
        '[i]': 'blue',        // INFO
        '[+]': 'green',       // ADDED
        '[-]': 'red',         // REMOVED
        '[C]': 'cyan',        // CHANGED
        '[U]': 'grey',        // UNCHANGED
        '[=]': 'grey',        // EQUAL
        '[/]': 'grey',        // SKIPPED
        '[V]': 'magenta',     // VERBOSE
        '[D]': 'magenta',     // DEBUG
        '[T]': 'magenta',     // TO-DO
        '[TODO]': 'magenta'   // TO-DO
    };

    for (var searchString in colorMap) {
        var color = colorMap[searchString];
        if (msg && msg.indexOf && msg.indexOf(searchString) > -1) {
            return chalk[color](msg);
        }
    }

    return msg;
};


//////////////////////////////////////////
// LOGGER FUNCTIONS                     //
//////////////////////////////////////////

/**
 * Gets the current config
 */
exports.getConfig = function() {
    return global.githubFannonSemlog.config;
};


/**
 * Gets the current config
 */
exports.getStatistics = function() {
    return global.githubFannonSemlog.statistics;
};

/**
 * Updates the config.
 * Only those parameters that have been given will be updated
 *
 * @param {object} config
 */
exports.updateConfig = function(config) {
    for (var key in config) {
        var value = config[key];
        global.githubFannonSemlog.config[key] = value;
    }
    return global.githubFannonSemlog.config;
};

/**
 * Clears (empties) the log object
 */
exports.clearLogHistory = function() {
    global.githubFannonSemlog.history = [];
    global.githubFannonSemlog.statistics = {
        debug: 0,
        warning: 0,
        error: 0,
        total: 0
    };
};

/**
 * Returns the global.moboLogObject
 *
 * @returns {Array}
 */
exports.getLogHistory = function() {
    return global.githubFannonSemlog.history;
};


//////////////////////////////////////////
// HELPER UTILITIES                     //
//////////////////////////////////////////

/**
 * Pad a number with n digits
 *
 * @param {number} number   number to pad
 * @param {number} digits   number of total digits
 * @returns {string}
 */
exports.pad = function(number, digits) {
    return new Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number;
};

/**
 * Adds dots as thousand separators to numbers
 *
 * http://stackoverflow.com/a/2901298
 *
 * @param number
 * @returns {string}
 */
exports.prettyNumber = function(number) {
    return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
};

/**
 * Replace all (/.../g) leading slash (^\/) or (|) trailing slash (\/$) with an empty string.
 *
 * @see http://stackoverflow.com/a/3840645
 *
 * @param {String} url URL / Path to cleanup

 * @returns {String}
 */
exports.cleanUrl = function(url) {
    url = url.trim();
    return url.replace(/^\/|\/$/g, '');
};


/**
 * Strips trailing slashes from URL
 *
 * @see http://stackoverflow.com/a/6680858/776425
 *
 * @param {String} url URL to cleanup
 * @returns {String}
 */
exports.stripTrailingSlash = function(url) {
    if (url.substr(-1) === '/') {
        url = url.substr(0, url.length - 1);
    }
    return url;
};

/**
 * Returns the byte length of an utf8 string or an object (when parsed to JSON)
 *
 * @see http://stackoverflow.com/a/23329386
 */
exports.byteSize = function(obj) {

    var str = '';

    if (typeof obj === 'object') {
        str = JSON.stringify(obj);
    } else {
        str = obj.toString();
    }

    var s = str.length;
    for (var i = str.length - 1; i >= 0; i--) {
        var code = str.charCodeAt(i);
        if (code > 0x7f && code <= 0x7ff) {
            s++;
        } else if (code > 0x7ff && code <= 0xffff) {
            s += 2;
        }
        if (code >= 0xDC00 && code <= 0xDFFF) {
            i--;
        } //trail surrogate
    }
    return s;
};

/**
 *
 * @param bytes
 * @param [si]
 * @returns {string}
 *
 * @see http://stackoverflow.com/a/14919494
 */
exports.prettyBytes = function(bytes, si) {
    var thresh = si ? 1000 : 1024;
    if (bytes < thresh) {
        return bytes + ' B';
    }
    var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
    var u = -1;
    do {
        bytes /= thresh;
        ++u;
    } while (bytes >= thresh);
    return bytes.toFixed(1) + ' ' + units[u];
};

/**
 * Returns an array with date / time information
 * Starts with year at index 0 up to index 6 for milliseconds
 *
 * @param {Date=} date   Optional date object. If falsy, will take current time.
 * @returns {Array}
 */
exports.getDateArray = function(date) {
    date = date || new Date();
    return [
        date.getFullYear(),
        exports.pad(date.getMonth() + 1, 2),
        exports.pad(date.getDate(), 2),
        exports.pad(date.getHours(), 2),
        exports.pad(date.getMinutes(), 2),
        exports.pad(date.getSeconds(), 2),
        exports.pad(date.getMilliseconds(), 2)
    ];
};

/**
 * Returns nicely formatted date-time
 * @example 2015-02-10 16:01:12
 *
 * @param {object} [date]
 * @returns {string}
 */
exports.humanDate = function(date) {
    date = date || new Date();
    var d = exports.getDateArray(date);
    return d[0] + '-' + d[1] + '-' + d[2] + ' ' + d[3] + ':' + d[4] + ':' + d[5];
};

/**
 * Returns a formatted date-time, optimized for machines
 * @example 2015-02-10_16-00-08
 *
 * @param {object} [date]
 * @returns {string}
 */
exports.roboDate = function(date) {
    date = date || new Date();
    var d = exports.getDateArray(date);
    return d[0] + '-' + d[1] + '-' + d[2] + '_' + d[3] + '-' + d[4] + '-' + d[5];
};

/**
 * Returns nicely formatted date-time
 * @example 16:01:12
 *
 * @param {object} [date]
 * @returns {string}
 */
exports.humanTime = function(date) {
    date = date || new Date();
    var d = exports.getDateArray(date);
    return d[3] + ':' + d[4] + ':' + d[5];
};

/**
 * Returns a formatted date-time, optimized for machines
 * @example 2015-02-10_16-00-08
 *
 * @param {object} [date]
 * @returns {string}
 */
exports.roboTime = function(date) {
    date = date || new Date();
    var d = exports.getDateArray(date);
    return d[3] + '-' + d[4] + '-' + d[5];
};