mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
The previous use of `Npm.require()` yielded at this point of logging, which could lead to logging out of order (!) and failing self-tests in particular.
277 lines
8.0 KiB
JavaScript
277 lines
8.0 KiB
JavaScript
Log = function () {
|
|
return Log.info.apply(this, arguments);
|
|
};
|
|
|
|
/// FOR TESTING
|
|
var intercept = 0;
|
|
var interceptedLines = [];
|
|
var suppress = 0;
|
|
|
|
// Intercept the next 'count' calls to a Log function. The actual
|
|
// lines printed to the console can be cleared and read by calling
|
|
// Log._intercepted().
|
|
Log._intercept = function (count) {
|
|
intercept += count;
|
|
};
|
|
|
|
// Suppress the next 'count' calls to a Log function. Use this to stop
|
|
// tests from spamming the console, especially with red errors that
|
|
// might look like a failing test.
|
|
Log._suppress = function (count) {
|
|
suppress += count;
|
|
};
|
|
|
|
// Returns intercepted lines and resets the intercept counter.
|
|
Log._intercepted = function () {
|
|
var lines = interceptedLines;
|
|
interceptedLines = [];
|
|
intercept = 0;
|
|
return lines;
|
|
};
|
|
|
|
// Either 'json' or 'colored-text'.
|
|
//
|
|
// When this is set to 'json', print JSON documents that are parsed by another
|
|
// process ('satellite' or 'meteor run'). This other process should call
|
|
// 'Log.format' for nice output.
|
|
//
|
|
// When this is set to 'colored-text', call 'Log.format' before printing.
|
|
// This should be used for logging from within satellite, since there is no
|
|
// other process that will be reading its standard output.
|
|
Log.outputFormat = 'json';
|
|
|
|
var LEVEL_COLORS = {
|
|
debug: 'green',
|
|
// leave info as the default color
|
|
warn: 'magenta',
|
|
error: 'red'
|
|
};
|
|
|
|
var META_COLOR = 'blue';
|
|
|
|
// XXX package
|
|
var RESTRICTED_KEYS = ['time', 'timeInexact', 'level', 'file', 'line',
|
|
'program', 'originApp', 'satellite', 'stderr'];
|
|
|
|
var FORMATTED_KEYS = RESTRICTED_KEYS.concat(['app', 'message']);
|
|
|
|
var logInBrowser = function (obj) {
|
|
var str = Log.format(obj);
|
|
|
|
// XXX Some levels should be probably be sent to the server
|
|
var level = obj.level;
|
|
|
|
if ((typeof console !== 'undefined') && console[level]) {
|
|
console[level](str);
|
|
} else {
|
|
// XXX Uses of Meteor._debug should probably be replaced by Log.debug or
|
|
// Log.info, and we should have another name for "do your best to
|
|
// call call console.log".
|
|
Meteor._debug(str);
|
|
}
|
|
};
|
|
|
|
// @returns {Object: { line: Number, file: String }}
|
|
Log._getCallerDetails = function () {
|
|
var getStack = function () {
|
|
// We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a
|
|
// pre-parsed stack) since it's impossible to compose it with the use of
|
|
// Error.prepareStackTrace used on the server for source maps.
|
|
var err = new Error;
|
|
var stack = err.stack;
|
|
return stack;
|
|
};
|
|
|
|
var stack = getStack();
|
|
|
|
if (!stack) return {};
|
|
|
|
var lines = stack.split('\n');
|
|
|
|
// looking for the first line outside the logging package (or an
|
|
// eval if we find that first)
|
|
var line;
|
|
for (var i = 1; i < lines.length; ++i) {
|
|
line = lines[i];
|
|
if (line.match(/^\s*at eval \(eval/)) {
|
|
return {file: "eval"};
|
|
}
|
|
|
|
if (!line.match(/packages\/(?:local-test[:_])?logging(?:\/|\.js)/))
|
|
break;
|
|
}
|
|
|
|
var details = {};
|
|
|
|
// The format for FF is 'functionName@filePath:lineNumber'
|
|
// The format for V8 is 'functionName (packages/logging/logging.js:81)' or
|
|
// 'packages/logging/logging.js:81'
|
|
var match = /(?:[@(]| at )([^(]+?):([0-9:]+)(?:\)|$)/.exec(line);
|
|
if (!match)
|
|
return details;
|
|
// in case the matched block here is line:column
|
|
details.line = match[2].split(':')[0];
|
|
|
|
// Possible format: https://foo.bar.com/scripts/file.js?random=foobar
|
|
// XXX: if you can write the following in better way, please do it
|
|
// XXX: what about evals?
|
|
details.file = match[1].split('/').slice(-1)[0].split('?')[0];
|
|
|
|
return details;
|
|
};
|
|
|
|
_.each(['debug', 'info', 'warn', 'error'], function (level) {
|
|
// @param arg {String|Object}
|
|
Log[level] = function (arg) {
|
|
if (suppress) {
|
|
suppress--;
|
|
return;
|
|
}
|
|
|
|
var intercepted = false;
|
|
if (intercept) {
|
|
intercept--;
|
|
intercepted = true;
|
|
}
|
|
|
|
var obj = (_.isObject(arg) && !_.isRegExp(arg) && !_.isDate(arg) ) ?
|
|
arg : {message: new String(arg).toString() };
|
|
|
|
_.each(RESTRICTED_KEYS, function (key) {
|
|
if (obj[key])
|
|
throw new Error("Can't set '" + key + "' in log message");
|
|
});
|
|
|
|
if (_.has(obj, 'message') && !_.isString(obj.message))
|
|
throw new Error("The 'message' field in log objects must be a string");
|
|
if (!obj.omitCallerDetails)
|
|
obj = _.extend(Log._getCallerDetails(), obj);
|
|
obj.time = new Date();
|
|
obj.level = level;
|
|
|
|
// XXX allow you to enable 'debug', probably per-package
|
|
if (level === 'debug')
|
|
return;
|
|
|
|
if (intercepted) {
|
|
interceptedLines.push(EJSON.stringify(obj));
|
|
} else if (Meteor.isServer) {
|
|
if (Log.outputFormat === 'colored-text') {
|
|
console.log(Log.format(obj, {color: true}));
|
|
} else if (Log.outputFormat === 'json') {
|
|
console.log(EJSON.stringify(obj));
|
|
} else {
|
|
throw new Error("Unknown logging output format: " + Log.outputFormat);
|
|
}
|
|
} else {
|
|
logInBrowser(obj);
|
|
}
|
|
};
|
|
});
|
|
|
|
// tries to parse line as EJSON. returns object if parse is successful, or null if not
|
|
Log.parse = function (line) {
|
|
var obj = null;
|
|
if (line && line.charAt(0) === '{') { // might be json generated from calling 'Log'
|
|
try { obj = EJSON.parse(line); } catch (e) {}
|
|
}
|
|
|
|
// XXX should probably check fields other than 'time'
|
|
if (obj && obj.time && (obj.time instanceof Date))
|
|
return obj;
|
|
else
|
|
return null;
|
|
};
|
|
|
|
// formats a log object into colored human and machine-readable text
|
|
Log.format = function (obj, options) {
|
|
obj = EJSON.clone(obj); // don't mutate the argument
|
|
options = options || {};
|
|
|
|
var time = obj.time;
|
|
if (!(time instanceof Date))
|
|
throw new Error("'time' must be a Date object");
|
|
var timeInexact = obj.timeInexact;
|
|
|
|
// store fields that are in FORMATTED_KEYS since we strip them
|
|
var level = obj.level || 'info';
|
|
var file = obj.file;
|
|
var lineNumber = obj.line;
|
|
var appName = obj.app || '';
|
|
var originApp = obj.originApp;
|
|
var message = obj.message || '';
|
|
var program = obj.program || '';
|
|
var satellite = obj.satellite;
|
|
var stderr = obj.stderr || '';
|
|
|
|
_.each(FORMATTED_KEYS, function(key) {
|
|
delete obj[key];
|
|
});
|
|
|
|
if (!_.isEmpty(obj)) {
|
|
if (message) message += " ";
|
|
message += EJSON.stringify(obj);
|
|
}
|
|
|
|
var pad2 = function(n) { return n < 10 ? '0' + n : n.toString(); };
|
|
var pad3 = function(n) { return n < 100 ? '0' + pad2(n) : n.toString(); };
|
|
|
|
var dateStamp = time.getFullYear().toString() +
|
|
pad2(time.getMonth() + 1 /*0-based*/) +
|
|
pad2(time.getDate());
|
|
var timeStamp = pad2(time.getHours()) +
|
|
':' +
|
|
pad2(time.getMinutes()) +
|
|
':' +
|
|
pad2(time.getSeconds()) +
|
|
'.' +
|
|
pad3(time.getMilliseconds());
|
|
|
|
// eg in San Francisco in June this will be '(-7)'
|
|
var utcOffsetStr = '(' + (-(new Date().getTimezoneOffset() / 60)) + ')';
|
|
|
|
var appInfo = '';
|
|
if (appName) appInfo += appName;
|
|
if (originApp && originApp !== appName) appInfo += ' via ' + originApp;
|
|
if (appInfo) appInfo = '[' + appInfo + '] ';
|
|
|
|
var sourceInfoParts = [];
|
|
if (program) sourceInfoParts.push(program);
|
|
if (file) sourceInfoParts.push(file);
|
|
if (lineNumber) sourceInfoParts.push(lineNumber);
|
|
var sourceInfo = _.isEmpty(sourceInfoParts) ?
|
|
'' : '(' + sourceInfoParts.join(':') + ') ';
|
|
|
|
if (satellite)
|
|
sourceInfo += ['[', satellite, ']'].join('');
|
|
|
|
var stderrIndicator = stderr ? '(STDERR) ' : '';
|
|
|
|
var metaPrefix = [
|
|
level.charAt(0).toUpperCase(),
|
|
dateStamp,
|
|
'-',
|
|
timeStamp,
|
|
utcOffsetStr,
|
|
timeInexact ? '? ' : ' ',
|
|
appInfo,
|
|
sourceInfo,
|
|
stderrIndicator].join('');
|
|
|
|
var prettify = function (line, color) {
|
|
return (options.color && Meteor.isServer && color) ?
|
|
require('cli-color')[color](line) : line;
|
|
};
|
|
|
|
return prettify(metaPrefix, options.metaColor || META_COLOR) +
|
|
prettify(message, LEVEL_COLORS[level]);
|
|
};
|
|
|
|
// Turn a line of text into a loggable object.
|
|
// @param line {String}
|
|
// @param override {Object}
|
|
Log.objFromText = function (line, override) {
|
|
var obj = {message: line, level: "info", time: new Date(), timeInexact: true};
|
|
return _.extend(obj, override);
|
|
};
|