/**
* @copyright 2016 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/**
* @module nmodule/js/rc/log/Log
*/
define([
'module',
'Promise',
'underscore',
'nmodule/js/rc/asyncUtils/asyncUtils',
'nmodule/js/rc/log/Level',
'nmodule/js/rc/log/handlers/consoleHandler' ], function (
module,
Promise,
_,
asyncUtils,
Level,
consoleHandler) {
'use strict';
const { contains, extend, findIndex, isFunction, map, once } = _;
const { doRequire } = asyncUtils;
const DEFAULT_LEVEL = Level.INFO;
const CONSOLE_LOG_NAME = 'browser.console';
const slice = Array.prototype.slice;
const configuredLogLevels = {};
var ticketId = 0;
/**
* Cache of Log promises by log name.
* @inner
* @type {Object.<string, Promise.<module:nmodule/js/rc/log/Log>>}
*/
var getLoggerPromises = {};
/**
* Responsible for actually publishing a log message to some destination
* (console, `baja.outln`, logfile, etc).
* @typedef {Object} module:nmodule/js/rc/log/Log~Handler
* @property {module:nmodule/js/rc/log/Log~PublishCallback} publish implements
* how log messages will be handled
*/
/**
* When adding a custom Handler, implement the `publish` function to define
* how log messages will be handled.
*
* @callback module:nmodule/js/rc/log/Log~PublishCallback
* @param {string} name log name
* @param {module:nmodule/js/rc/log/Log.Level} level log level
* @param {string} msg an already fully-formatted log message.
* @returns {Promise}
*/
/**
* Class for logging messages throughout Niagara JS apps. Do not instantiate
* this class directly: rather use the {@link module:nmodule/js/rc/log/Log.getLogger getLogger()} function.
*
* Logs support SLF4J-style parameterization. See example.
*
* @class
* @alias module:nmodule/js/rc/log/Log
* @see module:log
*
* @example
* <caption>Supports SLF4J-style format anchors.</caption>
* return Log.getLogger('my.package.name')
* .then(function (log) {
* return log.log(Log.Level.INFO, 'foo was {} and bar was {}', 'foo', 'bar');
* });
*
* @example
* <caption>Supports a trailing Error argument.</caption>
* doSomethingAsync()
* .catch(function (err) {
* return Log.logMessage('my.package.name', Log.Level.SEVERE,
* '{} rejected with error', 'doSomethingAsync', err);
* });
*
* @example
* <caption>Has convenience methods for behaving like the console.</caption>
* define(['nmodule/js/rc/log/Log'], function (console) {
* //Note that all of these create and return Promises behind the scenes.
* //The log name will be browser.console.
* console.log('this logs at', 'FINE', 'level');
* console.info('this logs at', 'INFO', 'level');
* console.warn('this logs at', 'WARNING', 'level');
* console.error('this logs at', 'SEVERE', 'level');
* });
*/
var Log = function Log(name, handlers) {
this.$name = name;
this.$handlers = handlers || [];
this.$timers = [];
};
var getConfiguredHandlers = once(function () {
// noinspection JSUnresolvedVariable
var c = module.config(),
handlers = (c && c.logHandlers) || [];
return Promise.all(handlers.map(doRequire));
});
var applyConfiguredLogLevels = once(function () {
// noinspection JSUnresolvedVariable
var c = module.config(),
logLevels = c && c.logLevels;
Log.$applyLogLevels(logLevels);
});
/**
* @private
* @param {Object} logLevels log name -> log level name map
*/
Log.$applyLogLevels = function (logLevels) {
extend(configuredLogLevels, logLevels);
};
/**
* Convenience method to stop you from having to resolve the `Log.getLogger()`
* promise every time you wish to log a message. This will combine the lookup
* and log into one step.
*
* Note that you cannot perform an `isLoggable()` check with this method, so
* if your log message is expensive to generate, you may want to fully resolve
* the logger first.
*
* @param {string} name
* @param {module:nmodule/js/rc/log/Log.Level} level
* @param {String} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise} promise to be resolved when the message has been logged
*/
Log.logMessage = function (name, level, msg, args) {
var slicedArgs = slice.call(arguments, 1);
return Log.getLogger(name)
.then(function (log) {
return log.log.apply(log, slicedArgs);
});
};
/**
* Logs a message to the `browser.console` log at `FINE` level.
* This matches a browser's `console.log` API.
*
* @returns {Promise}
* @example
* Log.log('this', 'is', 'a', 'fine', 'message');
*/
Log.log = function () {
return consoleLog(Level.FINE, arguments);
};
/**
* Logs a message to the `browser.console` log at `INFO` level.
* This matches a browser's `console.info` API.
*
* @returns {Promise}
* @example
* Log.info('this', 'is', 'an', 'info', 'message');
*/
Log.info = function () {
return consoleLog(Level.INFO, arguments);
};
/**
* Logs a message to the `browser.console` log at `WARNING` level.
* This matches a browser's `console.warn` API.
*
* @returns {Promise}
* @example
* Log.warn('this', 'is', 'a', 'warning', 'message');
*/
Log.warn = function (msg) {
return consoleLog(Level.WARNING, arguments);
};
/**
* Logs a message to the `browser.console` log at `SEVERE` level.
* This matches a browser's `console.error` API.
*
* @returns {Promise}
* @example
* Log.error('this', 'is', 'an', 'error', 'message');
*/
Log.error = function () {
return consoleLog(Level.SEVERE, arguments);
};
/**
* Resolve a Log instance with the given name.
*
* @param {string} name name for the log to retrieve. Calling `getLogger()`
* twice for the same name will resolve the same Log instance.
* @returns {Promise.<module:nmodule/js/rc/log/Log>}
*/
Log.getLogger = function (name) {
if (getLoggerPromises[name]) {
return getLoggerPromises[name];
}
applyConfiguredLogLevels();
var prom = getConfiguredHandlers()
.then(function (handlers) {
var log = new Log(name, handlers.concat(consoleHandler));
getLoggerPromises[name] = Promise.resolve(log);
return log;
});
getLoggerPromises[name] = prom;
return prom;
};
/**
* Add a new handler to this Log. The same handler instance cannot be added
* to the same log more than once.
* @param {module:nmodule/js/rc/log/Log~Handler} handler
*/
Log.prototype.addHandler = function (handler) {
if (!handler || !isFunction(handler.publish)) {
throw new Error('handler with publish function required');
}
var handlers = this.$handlers;
if (!contains(handlers, handler)) {
handlers.push(handler);
}
};
/**
* Get the level configured for this log.
* @returns {module:nmodule/js/rc/log/Log.Level}
*/
Log.prototype.getLevel = function () {
return this.$level ||
(this.$level = getLevel(this.$name, configuredLogLevels) || DEFAULT_LEVEL);
};
/**
* Get the log's name.
* @returns {string}
*/
Log.prototype.getName = function () {
return this.$name;
};
/**
* Return true if a log message at the given log level will actually be logged
* by this logger. Use this to improve performance if a log message would be
* expensive to create.
* @param {module:nmodule/js/rc/log/Log.Level|string} level
* @returns {boolean}
*/
Log.prototype.isLoggable = function (level) {
if (typeof level === 'string') {
level = Level[level];
}
if (!level) {
throw new Error('level required');
}
var myLevel = this.getLevel();
return myLevel !== Level.OFF && myLevel.intValue() <= level.intValue();
};
/**
* Log the given message, giving all `Handler`s attached to this Log a chance
* to publish it.
*
* @param {module:nmodule/js/rc/log/Log.Level|string} level Log level object,
* or the name of it as a string
* @param {String} msg log message
* @param {...*} [args] additional arguments to use for parameterization. If
* the final argument is an Error, it will be logged by itself.
*
* @returns {Promise} promise to be resolved when all handlers are finished
* publishing
*
* @example
* const name = promptForName();
* log.log(Level.FINE, 'Hello, {}!', name);
*
* try {
* doSomething();
* } catch (err) {
* log.log('SEVERE', 'An error occurred at {}', new Date(), err);
* }
*/
Log.prototype.log = function (level, msg, args) {
if (!this.isLoggable(level)) {
return Promise.resolve();
}
var name = this.getName();
let str;
return slf4jFormatAsync(msg, slice.call(arguments, 2))
.then((msgStr) => {
str = msgStr;
return Promise.all(map(this.$getHandlers(), function (h) {
return Promise.resolve(h.publish(name, Log.Level[level], str))
.catch(function (err) {
return consoleHandler.publish(CONSOLE_LOG_NAME, Level.SEVERE,
slf4jFormat('Log handler failed to publish message', [ err ]));
});
}));
});
};
/**
* @private
* @returns {Array.<module:nmodule/js/rc/log/Log~Handler>}
*/
Log.prototype.$getHandlers = function () {
return this.$handlers;
};
/**
* Starts timing a particular operation.
*
* @param {string} id a "unique" ID to describe this operation.
* @param {module:nmodule/js/rc/log/Log.Level|string} [level=INFO] Log level
* object, or the name of it as a string
* @param {string} [msg] a message to log. If omitted, will simply log the ID
* @param {...*} [args] additional arguments to format the message
* @returns {number} a ticket that may be passed to `timeEnd`
* @throws {Error} if ID not provided
*/
Log.prototype.time = function (id, level, msg, args) {
level = level || DEFAULT_LEVEL;
var timers = this.$timers;
var ticket = ++ticketId;
timers.push({ id: id, start: +new Date(), ticket: ticket, level: level });
if (msg || id) {
this.log.apply(this,
[ level, msg || id ].concat(Array.prototype.slice.call(arguments, 3)))
.catch(function (ignore) {});
}
return ticket;
};
/**
* Stops timing a particular operation.
*
* @param {string|number} [id] the ID passed to `time()` (if duplicates are
* found, this will stop the least recently started timer). Or, pass the exact
* ticket returned from `time()`. If omitted, this call will be a no-op (this
* makes it safe to wrap `time()` calls in isLoggable checks).
* @param {string} [msg="id: {}ms"] a message to log. For format arguments,
* the number of milliseconds elapsed is always the last.
* @param {...*} [args] any additional arguments to use to format the message
* - remember elapsed time will always be added
*/
Log.prototype.timeEnd = function (id, msg, args) {
if (!id) { return; }
var timer = this.$getTimer(id);
if (!timer) {
return this.log('WARNING', 'Timer {} does not exist', id);
}
var elapsedTime = this.$getElapsedTime(timer);
this.log.apply(this,
[ timer.level, msg || timer.id + ': {}ms' ]
.concat(Array.prototype.slice.call(arguments, 2))
.concat(elapsedTime))
.catch(function (ignore) {});
};
Log.prototype.timing = function (func, level, msg) {
var that = this;
var prom = Promise.resolve(func());
if (!that.isLoggable(level)) {
return prom;
}
var ticket = that.time('timing', level);
var args = Array.prototype.slice.call(arguments, 2);
return prom
.then(function (result) {
that.timeEnd.apply(that, [ ticket ].concat(args));
return result;
});
};
/**
* @private
* @returns {object}
*/
Log.prototype.$getTimer = function (id) {
if (!id) { throw new Error('id required'); }
var timers = this.$timers;
var index;
if (typeof id === 'number') {
index = findIndex(timers, function (timer) {
return timer.ticket === id;
});
} else {
index = findIndex(timers, function (timer) {
return timer.id === id;
});
}
if (index >= 0) {
var timer = timers[index];
timers.splice(index, 1);
return timer;
}
};
/**
* @private
* @param {object} timer
* @returns {number}
*/
Log.prototype.$getElapsedTime = function (timer) {
return +new Date() - timer.start;
};
/**
* Logs the given message with level `CONFIG`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.config = function (msg, args) {
return doLog(this, Level.CONFIG, arguments);
};
/**
* Logs the given message with level `FINE`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.fine = function (msg, args) {
return doLog(this, Level.FINE, arguments);
};
/**
* Logs the given message with level `FINER`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.finer = function (msg, args) {
return doLog(this, Level.FINER, arguments);
};
/**
* Logs the given message with level `FINEST`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.finest = function (msg, args) {
return doLog(this, Level.FINEST, arguments);
};
/**
* Logs the given message with level `INFO`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.info = function (msg, args) {
return doLog(this, Level.INFO, arguments);
};
/**
* Logs the given message with level `SEVERE`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.severe = function (msg, args) {
return doLog(this, Level.SEVERE, arguments);
};
/**
* Logs the given message with level `WARNING`.
* @param {string} msg log message
* @param {...*} [args] additional arguments to use for parameterization
* @returns {Promise}
* @see module:nmodule/js/rc/log/Log#log
*/
Log.prototype.warning = function (msg, args) {
return doLog(this, Level.WARNING, arguments);
};
/**
* Sets the logging level configured on this Log instance.
* @param {module:nmodule/js/rc/log/Log.Level} level
*/
Log.prototype.setLevel = function (level) {
this.$level = level;
};
function consoleLog(level, args) {
return Log.logMessage(CONSOLE_LOG_NAME, level,
slice.apply(args).map(formatArg).join(' '));
}
function doLog(log, level, args) {
return log.log.apply(log, [ level ].concat(slice.call(args)));
}
function getLevel(name, logLevels) {
if (!logLevels || typeof logLevels !== 'object') { return DEFAULT_LEVEL; }
var split = name.split('.'),
configuredLogNames = Object.keys(logLevels),
level = '',
longestMatch = [];
for (var i = 0, len = configuredLogNames.length; i < len; ++i) {
var configName = configuredLogNames[i],
configSplit = configName ? configName.split('.') : [];
if (split.slice(0, configSplit.length).join('.') === configName &&
configSplit.length >= longestMatch.length) {
level = logLevels[configName];
longestMatch = configSplit;
}
}
return Log.Level[level] || DEFAULT_LEVEL;
}
/**
* Async version of slf4jFormat in order to localize the error string
* @param {Error|String} msg the error or string to be sent to the log
* @param {Object} args any arguments needed in order to format the message
* @returns {Promise<String>}
*/
function slf4jFormatAsync(msg, args) {
if (msg instanceof Error && msg.name === 'LocalizableError' && typeof msg.toStack === 'function') {
return msg.toStack()
.then((stack) => {
return stack;
});
}
return Promise.resolve(slf4jFormat(msg, args));
}
/**
* Determines the error message to be sent to the log file
* @param {Error|String} msg the error or string that is to be logged
* @param {Object} args any arguments needed in the formatting of the error
* @returns {string}
*/
function slf4jFormat(msg, args) {
if (msg instanceof Error) {
return msg.stack || String(msg);
}
var i = -1, len = args.length;
var str = String(msg).replace(/[\\]?[\\]?\{\}/g, function (match) {
if (match[0] === '\\') {
if (match[1] === '\\') {
return '\\' + formatArg(args[++i]);
} else {
return '{}';
}
} else {
++i;
if (i >= len) {
return '{}';
} else {
return formatArg(args[i]);
}
}
});
//check for trailing Error arg and log it by itself
if (i === len - 2) {
var err = args[len - 1];
if (err instanceof Error) {
str += '\n' + err.stack;
}
}
return str;
}
function formatArg(obj) {
if (isFunction(obj)) {
return formatFunction(obj);
}
if (typeof obj === 'object' && obj &&
obj.toString === Object.prototype.toString) {
try {
return JSON.stringify(obj, (k, v) => {
if (isFunction(v)) {
return formatFunction(v);
}
return v;
});
} catch (e) {
Log.error(e);
return '{invalid JSON}';
}
}
if (Array.isArray(obj)) {
return '[' + obj.map(formatArg).join(',') + ']';
}
return String(obj);
}
function formatFunction(f) {
return '<function ' + f.name + '()>';
}
Log.formatArg = formatArg;
Log.Level = Level;
return Log;
});