forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlogging.js
274 lines (226 loc) · 7.87 KB
/
logging.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
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"};
}
// XXX probably wants to be / or .js in case no source maps
if (!line.match(/packages\/logging(?:\/|(?::tests)?\.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");
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 sourceInfo = (file && lineNumber) ?
['(', (program ? program + ':' : ''), file, ':', lineNumber, ') '].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) ?
Npm.require('cli-color')[color](line) : line;
};
return prettify(metaPrefix, 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);
};