RackHD/on-core

View on GitHub
lib/serializables/log-event.js

Summary

Maintainability
C
1 day
Test Coverage
// Copyright 2015, EMC, Inc.

'use strict';

module.exports = LogEventFactory;

LogEventFactory.$provide = 'LogEvent';
LogEventFactory.$inject = [
    'Errors',
    'Constants',
    'Serializable',
    'Services.Lookup',
    'Assert',
    '_',
    'Promise',
    'stack-trace',
    'colors',
    'prettyjson',
    'console'
];

function LogEventFactory (
    Errors,
    Constants,
    Serializable,
    lookupService,
    assert,
    _,
    Promise,
    stack,
    colors,
    pretty,
    console
) {
    //default turn off the colorful output to achieve tidy and clean log file.
    LogEvent.colorEnable = false;

    colors.setTheme(Constants.Logging.Colors); //setTheme will not impact color enable or not

    function LogEvent(defaults) {
        Serializable.call(this, LogEvent.schema, defaults);
    }

    LogEvent.schema = {
        id: 'LogEvent',
        type: 'object',
        properties: {
            package: {
                type: 'string',
            },
            host: {
                type: 'string'
            },
            level: {
                type: 'string',
                enum: _.keys(Constants.Logging.Levels)
            },
            message: {
                type: 'string'
            },
            context: {
                type: 'object'
            },
            timestamp: {
                type: 'string',
            },
            caller: {
                type: 'string'
            },
            subject: {
                type: 'string'
            }
        },
        required: [ 'level', 'timestamp', 'caller', 'subject' ]
    };

    Serializable.register(LogEventFactory, LogEvent);

    LogEvent.prototype.print = function () {
        var statement = [];

        this.context = LogEvent.redact(this.context);
        // The level length 8 is hardcoded here, as the loggest level up to now
        // is 8, this might be subject to change when new log levels are added
        // in the future
        statement.push('[%s]'.format( _.pad(this.level, 8)));
        statement.push('[%s]'.format(this.timestamp));
        statement.push('[%s]'.format(
            this.subject.substring(this.subject.length - Constants.Logging.Context.Length)
        ));
        statement.push(this.message);

        var output = statement.join(' ');

        console.log(colors[this.level](output));
        console.log(colors[this.level](' -> ' + this.caller));

        if (_.keys(this.context).length > 0) {
            console.log(
                pretty.render(
                    this.context,
                    {
                        noColor: !LogEvent.colorEnable,
                        numberColor: 'cyan'
                    }
                )
            );
        }

        return this;
    };

    LogEvent.create = function (options) {
        assert.object(options, 'options');

        return LogEvent.getSubject(options.context)
            .then(function(subject) {
                options.context = LogEvent.sanitize(options.context);
                return new LogEvent(_.merge(options, { subject: subject }));
            }).then(function (object) {
                return object.validate();
            });
    };

    /**
     * Set enabled or disabled for colorful log
     * @param {boolean} enable - true to enable colorful log; false to disable
     */
    LogEvent.setColorEnable = function(enable) {
        LogEvent.colorEnable = (enable ? true : false);
        colors.enabled = LogEvent.colorEnable;
    };

    LogEvent.sanitize = function(context) {
        return _.omit(context, Constants.Logging.Sanitizations);
    };

    /**
     * Initialize the redact regex
     *
     * By initializing all the redact 'RegExp' pattern in advance can speed up the subsequent
     * testing; Keep the 'String` pattern of its original type can leverage the string comparasion
     * which is faster than regex.
     *
     * @param {Array} redactions - The array of regex
     */
    LogEvent.initRedact = function(redactions) {
        LogEvent._redactPatterns = _.reduce(redactions, function(result, value) {
            if (_.isString(value)) {
                result.push(value); //exactly full word match
            } else if (_.isRegExp(value)) {
                result.push(new RegExp(value));
            } else {
                console.log("warning: unsupported redaction type '" + typeof(value) + "', " +
                    "only 'RegExp' and 'String' is allowed.");
            }
            return result;
        }, []);
    };

    /**
     * Test whether the value meet the redaction criteria
     * @param {String} value - the target value to test
     * @return {Boolean} true if it is need redaction, otherwise faluse
     */
    LogEvent.testRedact = function(value) {
        if (!_.isString(value)) {
            return false;
        }

        return _.some(LogEvent._redactPatterns, function(pattern) {
            if (_.isString(pattern)) {
                // The reason why use 'valueOf':
                // new String('abc') is not strictly equal to new String('abc'),
                // but new String('abc').valueOf() is strictly equal to new String('abc').valueOf()
                return (pattern.valueOf() === value.valueOf());
            }
            else { //Regex
                return pattern.test(value);
            }
        });
    };

    LogEvent.redact = function (target) {
        return LogEvent._redact(_.cloneDeep(target));
    };

    LogEvent._redact = function (target) {
        return _.transform(target, function (accumulator, value, key) {
            if (LogEvent.testRedact(key)) {
                accumulator[key] = '[REDACTED]';
            } else {
                accumulator[key] = value;
            }

            if (_.isObject(accumulator[key]) || _.isArray(accumulator[key])) {
                accumulator[key] = LogEvent._redact(accumulator[key]);
            }
        }, target || {});
    };

    LogEvent.getUniqueId = function(context) {
        if (_.isEmpty(context)) {
            return Promise.resolve();
        }

        if (context.id) {
            return Promise.resolve(context.id);
        }

        if (context.macaddress) {
            return lookupService.macAddressToNodeId(context.macaddress)
                .then(function (identifier) {
                    return identifier;
                });
        }

        if (context.ip) {
            return lookupService.ipAddressToNodeId(context.ip)
                .then(function (identifier) {
                    return identifier;
                });
        }
        // NOTE: This should not be reached
        return Promise.resolve();
    };

    LogEvent.getSubject = function(context) {
        var defaultSubject = 'Server';

        return LogEvent.getUniqueId(context)
            .then(function(subject) {
                return subject || defaultSubject;
            })
            .catch(function() {
                return defaultSubject;
            });
    };

    LogEvent.initRedact(Constants.Logging.Redactions);
    return LogEvent;
}