/** @module */
'use strict';
require('../../core-upgrade.js');
var LogData = require('./LogData.js').LogData;
var Promise = require('../utils/promise.js');
var JSUtils = require('../utils/jsutils.js').JSUtils;
/**
* Multi-purpose logger. Supports different kinds of logging (errors,
* warnings, fatal errors, etc.) and a variety of logging data (errors,
* strings, objects).
*
* @class
* @param {Object} [opts] Logger options (not used by superclass).
*/
var Logger = function(opts) {
if (!opts) { opts = {}; }
this._opts = opts;
this._logRequestQueue = [];
this._backends = new Map();
// Set up regular expressions so that logTypes can be registered with
// backends, and so that logData can be routed to the right backends.
// Default: matches empty string only
this._testAllRE = new RegExp(/^$/);
this._samplers = [];
this._samplersRE = new RegExp(/^$/);
this._samplersCache = new Map();
};
Logger.prototype._createLogData = function(logType, logObject) {
return new LogData(logType, logObject);
};
/**
* Outputs logging and tracing information to different backends.
* @param {string} logType
* @return {undefined|Promise} a {@link Promise} that will be fulfilled when all
* logging is complete; for efficiency `undefined` is returned if this
* `logType` is being ignored (the common case).
*/
Logger.prototype.log = function(logType) {
try {
// Potentially return early if we're sampling this log type.
if (this._samplersRE.test(logType) &&
!/^fatal/.test(logType) // No sampling for fatals.
) {
if (!this._samplersCache.has(logType)) {
var i = 0;
var len = this._samplers.length;
for (; i < len; i++) {
var sample = this._samplers[i];
if (sample.logTypeRE.test(logType)) {
this._samplersCache.set(logType, sample.percent);
break; // Use the first applicable rate.
}
}
console.assert(i < len,
'Odd, couldn\'t find the sample rate for: ' + logType);
}
// This works because it's [0, 100)
if ((Math.random() * 100) >= this._samplersCache.get(logType)) {
return;
}
}
// XXX this should be configurable.
// Tests whether logType matches any of the applicable logTypes
if (this._testAllRE.test(logType)) {
var logObject = Array.prototype.slice.call(arguments, 1);
var logData = this._createLogData(logType, logObject);
// If we are already processing a log request, but a log was generated
// while processing the first request, processingLogRequest will be true.
// We ignore all follow-on log events unless they are fatal. We put all
// fatal log events on the logRequestQueue for processing later on.
if (this.processingLogRequest) {
if (/^fatal$/.test(logType)) {
// Array.from converts arguments to a real array
// So that arguments can later be used in log.apply
this._logRequestQueue.push(Array.from(arguments));
// Create a deferred, which will be resolved when this
// data is finally logged.
var d = Promise.defer();
this._logRequestQueue.push(d);
return d.promise;
}
return; // ignored
} else {
// We weren't already processing a request, so processingLogRequest flag
// is set to true. Then we send the logData to appropriate backends and
// process any fatal log events that we find on the queue.
this.processingLogRequest = true;
// Callback to routeToBackends forces logging of fatal log events.
var p = this._routeToBackends(logData);
this.processingLogRequest = false;
if (this._logRequestQueue.length > 0) {
var args = this._logRequestQueue.pop();
var dd = this._logRequestQueue.pop();
this.log.apply(this, args).then(dd.resolve, dd.reject);
}
return p; // could be undefined, if no backends handled this
}
}
} catch (e) {
console.log(e.message);
console.log(e.stack);
}
return; // nothing handled this log type
};
/**
* Convert logType into a source string for a regExp that we can
* subsequently use to test logTypes passed in from Logger.log.
* @param {RegExp} logType
* @return {string}
* @private
*/
function logTypeToString(logType) {
var logTypeString;
if (logType instanceof RegExp) {
logTypeString = logType.source;
} else if (typeof (logType) === 'string') {
logTypeString = '^' + JSUtils.escapeRegExp(logType) + '$';
} else {
throw new Error('logType is neither a regular expression nor a string.');
}
return logTypeString;
}
/**
* Logger backend.
* @callback module:logger/Logger~backendCallback
* @param {LogData} logData The data to log.
* @return {Promise} A {@link Promise} that is fulfilled when logging of this
* `logData` is complete.
*/
/**
* Registers a backend by adding it to the collection of backends.
* @param {RegExp} logType
* @param {backendCallback} backend Backend to send logging / tracing info to.
*/
Logger.prototype.registerBackend = function(logType, backend) {
var backendArray = [];
var logTypeString = logTypeToString(logType);
// If we've already started an array of backends for this logType,
// add this backend to the array; otherwise, start a new array
// consisting of this backend.
if (this._backends.has(logTypeString)) {
backendArray = this._backends.get(logTypeString);
}
if (backendArray.indexOf(backend) === -1) {
backendArray.push(backend);
}
this._backends.set(logTypeString, backendArray);
// Update the global test RE
this._testAllRE = new RegExp(this._testAllRE.source + "|" + logTypeString);
};
/**
* Register sampling rates, in percent, for log types.
* @param {RegExp} logType
* @param {number} percent
*/
Logger.prototype.registerSampling = function(logType, percent) {
var logTypeString = logTypeToString(logType);
percent = Number(percent);
if (Number.isNaN(percent) || percent < 0 || percent > 100) {
throw new Error('Sampling rate for ' + logType +
' is not a percentage: ' + percent);
}
this._samplers.push({ logTypeRE: new RegExp(logTypeString), percent: percent });
this._samplersRE = new RegExp(this._samplersRE.source + '|' + logTypeString);
};
/** @return {backendCallback} */
Logger.prototype.getDefaultBackend = function() {
return logData => this._defaultBackend(logData);
};
/** @return {backendCallback} */
Logger.prototype.getDefaultTracerBackend = function() {
return logData => this._defaultTracerBackend(logData);
};
/**
* Optional default backend.
* @method
* @param {LogData} logData
* @return {Promise} Promise which is fulfilled when logging is complete.
*/
Logger.prototype._defaultBackend = Promise.async(function *(logData) { // eslint-disable-line require-yield
// Wrap in try-catch-finally so we can more accurately
// pin backend crashers on specific logging backends.
try {
console.warn('[' + logData.logType + '] ' + logData.fullMsg());
} catch (e) {
console.error("Error in Logger._defaultBackend: " + e);
}
});
/**
* Optional default tracing and debugging backend.
* @method
* @param {LogData} logData
* @return {Promise} Promise which is fulfilled when logging is complete.
*/
Logger.prototype._defaultTracerBackend = Promise.async(function *(logData) { // eslint-disable-line require-yield
try {
var logType = logData.logType;
// indent by number of slashes
var indent = ' '.repeat(logType.match(/\//g).length - 1);
// XXX: could shorten or strip trace/ logType prefix in a pure trace logger
var msg = indent + logType;
// Fixed-width type column so that the messages align
var typeColumnWidth = 30;
msg = msg.substr(0, typeColumnWidth);
msg += ' '.repeat(typeColumnWidth - msg.length);
msg += '| ' + indent + logData.msg();
if (msg) {
console.warn(msg);
}
} catch (e) {
console.error("Error in Logger._defaultTracerBackend: " + e);
}
});
/**
* Gets all registered backends that apply to a particular logType.
* @param {LogData} logData
* @return {Generator.<backendCallback>}
*/
Logger.prototype._getApplicableBackends = function *(logData) {
var logType = logData.logType;
var backendsMap = this._backends;
var logTypeString;
for (logTypeString of backendsMap.keys()) {
// Convert the stored logTypeString back into a regExp, in case
// it applies to multiple logTypes (e.g. /fatal|error/).
if (new RegExp(logTypeString).test(logType)) {
yield* backendsMap.get(logTypeString);
}
}
};
/**
* Routes log data to backends. If `logData.logType` is fatal, exits process
* after logging to all backends.
* @param {LogData} logData
* @return {Promise|undefined} A {@link Promise} that is fulfilled when all
* logging is complete, or `undefined` if no backend was applicable and
* the `logType` was not fatal (fast path common case).
*/
Logger.prototype._routeToBackends = function(logData) {
var applicableBackends = Array.from(this._getApplicableBackends(logData));
var noop = function() {};
// fast path!
if (applicableBackends.length === 0 && !/^fatal$/.test(logData.logType)) {
return; // no promise allocated on fast path.
}
// If the logType is fatal, exits the process after logging
// to all of the backends.
// Additionally runs a callback that looks for fatal
// events in the queue and logs them.
return Promise.all(applicableBackends.map(function(backend) {
var d = Promise.defer();
var p = d.promise;
var r;
try {
// For backward-compatibility, pass in a callback as the 2nd arg
// (it should be ignored by current backends)
r = backend(logData, d.resolve);
} catch (e) {
// ignore any exceptions thrown while calling 'backend'
}
// Backends *should* return a Promise... but for backward-compatibility
// don't fret if they don't.
if (r && typeof (r) === 'object' && r.then) {
p = Promise.race([p, r]);
}
// The returned promise should always resolve, never reject.
return p.catch(noop);
})).finally(function() {
if (/^fatal$/.test(logData.logType)) {
// Give some time for async loggers to deliver the message
setTimeout(function() { process.exit(1); }, 100);
}
});
};
if (typeof module === "object") {
module.exports.Logger = Logger;
}