|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["Log"]; |
|
8 |
|
9 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; |
|
10 |
|
11 const ONE_BYTE = 1; |
|
12 const ONE_KILOBYTE = 1024 * ONE_BYTE; |
|
13 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; |
|
14 |
|
15 const STREAM_SEGMENT_SIZE = 4096; |
|
16 const PR_UINT32_MAX = 0xffffffff; |
|
17 |
|
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
20 "resource://gre/modules/osfile.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
22 "resource://gre/modules/Task.jsm"); |
|
23 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]); |
|
24 |
|
25 |
|
26 /* |
|
27 * Dump a message everywhere we can if we have a failure. |
|
28 */ |
|
29 function dumpError(text) { |
|
30 dump(text + "\n"); |
|
31 Cu.reportError(text); |
|
32 } |
|
33 |
|
34 this.Log = { |
|
35 Level: { |
|
36 Fatal: 70, |
|
37 Error: 60, |
|
38 Warn: 50, |
|
39 Info: 40, |
|
40 Config: 30, |
|
41 Debug: 20, |
|
42 Trace: 10, |
|
43 All: 0, |
|
44 Desc: { |
|
45 70: "FATAL", |
|
46 60: "ERROR", |
|
47 50: "WARN", |
|
48 40: "INFO", |
|
49 30: "CONFIG", |
|
50 20: "DEBUG", |
|
51 10: "TRACE", |
|
52 0: "ALL" |
|
53 }, |
|
54 Numbers: { |
|
55 "FATAL": 70, |
|
56 "ERROR": 60, |
|
57 "WARN": 50, |
|
58 "INFO": 40, |
|
59 "CONFIG": 30, |
|
60 "DEBUG": 20, |
|
61 "TRACE": 10, |
|
62 "ALL": 0, |
|
63 } |
|
64 }, |
|
65 |
|
66 get repository() { |
|
67 delete Log.repository; |
|
68 Log.repository = new LoggerRepository(); |
|
69 return Log.repository; |
|
70 }, |
|
71 set repository(value) { |
|
72 delete Log.repository; |
|
73 Log.repository = value; |
|
74 }, |
|
75 |
|
76 LogMessage: LogMessage, |
|
77 Logger: Logger, |
|
78 LoggerRepository: LoggerRepository, |
|
79 |
|
80 Formatter: Formatter, |
|
81 BasicFormatter: BasicFormatter, |
|
82 MessageOnlyFormatter: MessageOnlyFormatter, |
|
83 StructuredFormatter: StructuredFormatter, |
|
84 |
|
85 Appender: Appender, |
|
86 DumpAppender: DumpAppender, |
|
87 ConsoleAppender: ConsoleAppender, |
|
88 StorageStreamAppender: StorageStreamAppender, |
|
89 |
|
90 FileAppender: FileAppender, |
|
91 BoundedFileAppender: BoundedFileAppender, |
|
92 |
|
93 ParameterFormatter: ParameterFormatter, |
|
94 // Logging helper: |
|
95 // let logger = Log.repository.getLogger("foo"); |
|
96 // logger.info(Log.enumerateInterfaces(someObject).join(",")); |
|
97 enumerateInterfaces: function Log_enumerateInterfaces(aObject) { |
|
98 let interfaces = []; |
|
99 |
|
100 for (i in Ci) { |
|
101 try { |
|
102 aObject.QueryInterface(Ci[i]); |
|
103 interfaces.push(i); |
|
104 } |
|
105 catch(ex) {} |
|
106 } |
|
107 |
|
108 return interfaces; |
|
109 }, |
|
110 |
|
111 // Logging helper: |
|
112 // let logger = Log.repository.getLogger("foo"); |
|
113 // logger.info(Log.enumerateProperties(someObject).join(",")); |
|
114 enumerateProperties: function (aObject, aExcludeComplexTypes) { |
|
115 let properties = []; |
|
116 |
|
117 for (p in aObject) { |
|
118 try { |
|
119 if (aExcludeComplexTypes && |
|
120 (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function")) |
|
121 continue; |
|
122 properties.push(p + " = " + aObject[p]); |
|
123 } |
|
124 catch(ex) { |
|
125 properties.push(p + " = " + ex); |
|
126 } |
|
127 } |
|
128 |
|
129 return properties; |
|
130 }, |
|
131 |
|
132 _formatError: function _formatError(e) { |
|
133 let result = e.toString(); |
|
134 if (e.fileName) { |
|
135 result += " (" + e.fileName; |
|
136 if (e.lineNumber) { |
|
137 result += ":" + e.lineNumber; |
|
138 } |
|
139 if (e.columnNumber) { |
|
140 result += ":" + e.columnNumber; |
|
141 } |
|
142 result += ")"; |
|
143 } |
|
144 return result + " " + Log.stackTrace(e); |
|
145 }, |
|
146 |
|
147 // This is for back compatibility with services/common/utils.js; we duplicate |
|
148 // some of the logic in ParameterFormatter |
|
149 exceptionStr: function exceptionStr(e) { |
|
150 if (!e) { |
|
151 return "" + e; |
|
152 } |
|
153 if (e instanceof Ci.nsIException) { |
|
154 return e.toString() + " " + Log.stackTrace(e); |
|
155 } |
|
156 else if (isError(e)) { |
|
157 return Log._formatError(e); |
|
158 } |
|
159 // else |
|
160 let message = e.message ? e.message : e; |
|
161 return message + " " + Log.stackTrace(e); |
|
162 }, |
|
163 |
|
164 stackTrace: function stackTrace(e) { |
|
165 // Wrapped nsIException |
|
166 if (e.location) { |
|
167 let frame = e.location; |
|
168 let output = []; |
|
169 while (frame) { |
|
170 // Works on frames or exceptions, munges file:// URIs to shorten the paths |
|
171 // FIXME: filename munging is sort of hackish, might be confusing if |
|
172 // there are multiple extensions with similar filenames |
|
173 let str = "<file:unknown>"; |
|
174 |
|
175 let file = frame.filename || frame.fileName; |
|
176 if (file) { |
|
177 str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1"); |
|
178 } |
|
179 |
|
180 if (frame.lineNumber) { |
|
181 str += ":" + frame.lineNumber; |
|
182 } |
|
183 |
|
184 if (frame.name) { |
|
185 str = frame.name + "()@" + str; |
|
186 } |
|
187 |
|
188 if (str) { |
|
189 output.push(str); |
|
190 } |
|
191 frame = frame.caller; |
|
192 } |
|
193 return "Stack trace: " + output.join(" < "); |
|
194 } |
|
195 // Standard JS exception |
|
196 if (e.stack) { |
|
197 return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). |
|
198 replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1"); |
|
199 } |
|
200 |
|
201 return "No traceback available"; |
|
202 } |
|
203 }; |
|
204 |
|
205 /* |
|
206 * LogMessage |
|
207 * Encapsulates a single log event's data |
|
208 */ |
|
209 function LogMessage(loggerName, level, message, params) { |
|
210 this.loggerName = loggerName; |
|
211 this.level = level; |
|
212 /* |
|
213 * Special case to handle "log./level/(object)", for example logging a caught exception |
|
214 * without providing text or params like: catch(e) { logger.warn(e) } |
|
215 * Treating this as an empty text with the object in the 'params' field causes the |
|
216 * object to be formatted properly by BasicFormatter. |
|
217 */ |
|
218 if (!params && message && (typeof(message) == "object") && |
|
219 (typeof(message.valueOf()) != "string")) { |
|
220 this.message = null; |
|
221 this.params = message; |
|
222 } else { |
|
223 // If the message text is empty, or a string, or a String object, normal handling |
|
224 this.message = message; |
|
225 this.params = params; |
|
226 } |
|
227 |
|
228 // The _structured field will correspond to whether this message is to |
|
229 // be interpreted as a structured message. |
|
230 this._structured = this.params && this.params.action; |
|
231 this.time = Date.now(); |
|
232 } |
|
233 LogMessage.prototype = { |
|
234 get levelDesc() { |
|
235 if (this.level in Log.Level.Desc) |
|
236 return Log.Level.Desc[this.level]; |
|
237 return "UNKNOWN"; |
|
238 }, |
|
239 |
|
240 toString: function LogMsg_toString() { |
|
241 let msg = "LogMessage [" + this.time + " " + this.level + " " + |
|
242 this.message; |
|
243 if (this.params) { |
|
244 msg += " " + JSON.stringify(this.params); |
|
245 } |
|
246 return msg + "]" |
|
247 } |
|
248 }; |
|
249 |
|
250 /* |
|
251 * Logger |
|
252 * Hierarchical version. Logs to all appenders, assigned or inherited |
|
253 */ |
|
254 |
|
255 function Logger(name, repository) { |
|
256 if (!repository) |
|
257 repository = Log.repository; |
|
258 this._name = name; |
|
259 this.children = []; |
|
260 this.ownAppenders = []; |
|
261 this.appenders = []; |
|
262 this._repository = repository; |
|
263 } |
|
264 Logger.prototype = { |
|
265 get name() { |
|
266 return this._name; |
|
267 }, |
|
268 |
|
269 _level: null, |
|
270 get level() { |
|
271 if (this._level != null) |
|
272 return this._level; |
|
273 if (this.parent) |
|
274 return this.parent.level; |
|
275 dumpError("Log warning: root logger configuration error: no level defined"); |
|
276 return Log.Level.All; |
|
277 }, |
|
278 set level(level) { |
|
279 this._level = level; |
|
280 }, |
|
281 |
|
282 _parent: null, |
|
283 get parent() this._parent, |
|
284 set parent(parent) { |
|
285 if (this._parent == parent) { |
|
286 return; |
|
287 } |
|
288 // Remove ourselves from parent's children |
|
289 if (this._parent) { |
|
290 let index = this._parent.children.indexOf(this); |
|
291 if (index != -1) { |
|
292 this._parent.children.splice(index, 1); |
|
293 } |
|
294 } |
|
295 this._parent = parent; |
|
296 parent.children.push(this); |
|
297 this.updateAppenders(); |
|
298 }, |
|
299 |
|
300 updateAppenders: function updateAppenders() { |
|
301 if (this._parent) { |
|
302 let notOwnAppenders = this._parent.appenders.filter(function(appender) { |
|
303 return this.ownAppenders.indexOf(appender) == -1; |
|
304 }, this); |
|
305 this.appenders = notOwnAppenders.concat(this.ownAppenders); |
|
306 } else { |
|
307 this.appenders = this.ownAppenders.slice(); |
|
308 } |
|
309 |
|
310 // Update children's appenders. |
|
311 for (let i = 0; i < this.children.length; i++) { |
|
312 this.children[i].updateAppenders(); |
|
313 } |
|
314 }, |
|
315 |
|
316 addAppender: function Logger_addAppender(appender) { |
|
317 if (this.ownAppenders.indexOf(appender) != -1) { |
|
318 return; |
|
319 } |
|
320 this.ownAppenders.push(appender); |
|
321 this.updateAppenders(); |
|
322 }, |
|
323 |
|
324 removeAppender: function Logger_removeAppender(appender) { |
|
325 let index = this.ownAppenders.indexOf(appender); |
|
326 if (index == -1) { |
|
327 return; |
|
328 } |
|
329 this.ownAppenders.splice(index, 1); |
|
330 this.updateAppenders(); |
|
331 }, |
|
332 |
|
333 /** |
|
334 * Logs a structured message object. |
|
335 * |
|
336 * @param action |
|
337 * (string) A message action, one of a set of actions known to the |
|
338 * log consumer. |
|
339 * @param params |
|
340 * (object) Parameters to be included in the message. |
|
341 * If _level is included as a key and the corresponding value |
|
342 * is a number or known level name, the message will be logged |
|
343 * at the indicated level. If _message is included as a key, the |
|
344 * value is used as the descriptive text for the message. |
|
345 */ |
|
346 logStructured: function (action, params) { |
|
347 if (!action) { |
|
348 throw "An action is required when logging a structured message."; |
|
349 } |
|
350 if (!params) { |
|
351 return this.log(this.level, undefined, {"action": action}); |
|
352 } |
|
353 if (typeof(params) != "object") { |
|
354 throw "The params argument is required to be an object."; |
|
355 } |
|
356 |
|
357 let level = params._level; |
|
358 if (level) { |
|
359 let ulevel = level.toUpperCase(); |
|
360 if (ulevel in Log.Level.Numbers) { |
|
361 level = Log.Level.Numbers[ulevel]; |
|
362 } |
|
363 } else { |
|
364 level = this.level; |
|
365 } |
|
366 |
|
367 params.action = action; |
|
368 this.log(level, params._message, params); |
|
369 }, |
|
370 |
|
371 log: function (level, string, params) { |
|
372 if (this.level > level) |
|
373 return; |
|
374 |
|
375 // Hold off on creating the message object until we actually have |
|
376 // an appender that's responsible. |
|
377 let message; |
|
378 let appenders = this.appenders; |
|
379 for (let appender of appenders) { |
|
380 if (appender.level > level) { |
|
381 continue; |
|
382 } |
|
383 if (!message) { |
|
384 message = new LogMessage(this._name, level, string, params); |
|
385 } |
|
386 appender.append(message); |
|
387 } |
|
388 }, |
|
389 |
|
390 fatal: function (string, params) { |
|
391 this.log(Log.Level.Fatal, string, params); |
|
392 }, |
|
393 error: function (string, params) { |
|
394 this.log(Log.Level.Error, string, params); |
|
395 }, |
|
396 warn: function (string, params) { |
|
397 this.log(Log.Level.Warn, string, params); |
|
398 }, |
|
399 info: function (string, params) { |
|
400 this.log(Log.Level.Info, string, params); |
|
401 }, |
|
402 config: function (string, params) { |
|
403 this.log(Log.Level.Config, string, params); |
|
404 }, |
|
405 debug: function (string, params) { |
|
406 this.log(Log.Level.Debug, string, params); |
|
407 }, |
|
408 trace: function (string, params) { |
|
409 this.log(Log.Level.Trace, string, params); |
|
410 } |
|
411 }; |
|
412 |
|
413 /* |
|
414 * LoggerRepository |
|
415 * Implements a hierarchy of Loggers |
|
416 */ |
|
417 |
|
418 function LoggerRepository() {} |
|
419 LoggerRepository.prototype = { |
|
420 _loggers: {}, |
|
421 |
|
422 _rootLogger: null, |
|
423 get rootLogger() { |
|
424 if (!this._rootLogger) { |
|
425 this._rootLogger = new Logger("root", this); |
|
426 this._rootLogger.level = Log.Level.All; |
|
427 } |
|
428 return this._rootLogger; |
|
429 }, |
|
430 set rootLogger(logger) { |
|
431 throw "Cannot change the root logger"; |
|
432 }, |
|
433 |
|
434 _updateParents: function LogRep__updateParents(name) { |
|
435 let pieces = name.split('.'); |
|
436 let cur, parent; |
|
437 |
|
438 // find the closest parent |
|
439 // don't test for the logger name itself, as there's a chance it's already |
|
440 // there in this._loggers |
|
441 for (let i = 0; i < pieces.length - 1; i++) { |
|
442 if (cur) |
|
443 cur += '.' + pieces[i]; |
|
444 else |
|
445 cur = pieces[i]; |
|
446 if (cur in this._loggers) |
|
447 parent = cur; |
|
448 } |
|
449 |
|
450 // if we didn't assign a parent above, there is no parent |
|
451 if (!parent) |
|
452 this._loggers[name].parent = this.rootLogger; |
|
453 else |
|
454 this._loggers[name].parent = this._loggers[parent]; |
|
455 |
|
456 // trigger updates for any possible descendants of this logger |
|
457 for (let logger in this._loggers) { |
|
458 if (logger != name && logger.indexOf(name) == 0) |
|
459 this._updateParents(logger); |
|
460 } |
|
461 }, |
|
462 |
|
463 /** |
|
464 * Obtain a named Logger. |
|
465 * |
|
466 * The returned Logger instance for a particular name is shared among |
|
467 * all callers. In other words, if two consumers call getLogger("foo"), |
|
468 * they will both have a reference to the same object. |
|
469 * |
|
470 * @return Logger |
|
471 */ |
|
472 getLogger: function (name) { |
|
473 if (name in this._loggers) |
|
474 return this._loggers[name]; |
|
475 this._loggers[name] = new Logger(name, this); |
|
476 this._updateParents(name); |
|
477 return this._loggers[name]; |
|
478 }, |
|
479 |
|
480 /** |
|
481 * Obtain a Logger that logs all string messages with a prefix. |
|
482 * |
|
483 * A common pattern is to have separate Logger instances for each instance |
|
484 * of an object. But, you still want to distinguish between each instance. |
|
485 * Since Log.repository.getLogger() returns shared Logger objects, |
|
486 * monkeypatching one Logger modifies them all. |
|
487 * |
|
488 * This function returns a new object with a prototype chain that chains |
|
489 * up to the original Logger instance. The new prototype has log functions |
|
490 * that prefix content to each message. |
|
491 * |
|
492 * @param name |
|
493 * (string) The Logger to retrieve. |
|
494 * @param prefix |
|
495 * (string) The string to prefix each logged message with. |
|
496 */ |
|
497 getLoggerWithMessagePrefix: function (name, prefix) { |
|
498 let log = this.getLogger(name); |
|
499 |
|
500 let proxy = {__proto__: log}; |
|
501 |
|
502 for (let level in Log.Level) { |
|
503 if (level == "Desc") { |
|
504 continue; |
|
505 } |
|
506 |
|
507 let lc = level.toLowerCase(); |
|
508 proxy[lc] = function (msg, ...args) { |
|
509 return log[lc].apply(log, [prefix + msg, ...args]); |
|
510 }; |
|
511 } |
|
512 |
|
513 return proxy; |
|
514 }, |
|
515 }; |
|
516 |
|
517 /* |
|
518 * Formatters |
|
519 * These massage a LogMessage into whatever output is desired. |
|
520 * BasicFormatter and StructuredFormatter are implemented here. |
|
521 */ |
|
522 |
|
523 // Abstract formatter |
|
524 function Formatter() {} |
|
525 Formatter.prototype = { |
|
526 format: function Formatter_format(message) {} |
|
527 }; |
|
528 |
|
529 // Basic formatter that doesn't do anything fancy. |
|
530 function BasicFormatter(dateFormat) { |
|
531 if (dateFormat) { |
|
532 this.dateFormat = dateFormat; |
|
533 } |
|
534 this.parameterFormatter = new ParameterFormatter(); |
|
535 } |
|
536 BasicFormatter.prototype = { |
|
537 __proto__: Formatter.prototype, |
|
538 |
|
539 /** |
|
540 * Format the text of a message with optional parameters. |
|
541 * If the text contains ${identifier}, replace that with |
|
542 * the value of params[identifier]; if ${}, replace that with |
|
543 * the entire params object. If no params have been substituted |
|
544 * into the text, format the entire object and append that |
|
545 * to the message. |
|
546 */ |
|
547 formatText: function (message) { |
|
548 let params = message.params; |
|
549 if (!params) { |
|
550 return message.message || ""; |
|
551 } |
|
552 // Defensive handling of non-object params |
|
553 // We could add a special case for NSRESULT values here... |
|
554 let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function'); |
|
555 |
|
556 // if we have params, try and find substitutions. |
|
557 if (message.params && this.parameterFormatter) { |
|
558 // have we successfully substituted any parameters into the message? |
|
559 // in the log message |
|
560 let subDone = false; |
|
561 let regex = /\$\{(\S*)\}/g; |
|
562 let textParts = []; |
|
563 if (message.message) { |
|
564 textParts.push(message.message.replace(regex, (_, sub) => { |
|
565 // ${foo} means use the params['foo'] |
|
566 if (sub) { |
|
567 if (pIsObject && sub in message.params) { |
|
568 subDone = true; |
|
569 return this.parameterFormatter.format(message.params[sub]); |
|
570 } |
|
571 return '${' + sub + '}'; |
|
572 } |
|
573 // ${} means use the entire params object. |
|
574 subDone = true; |
|
575 return this.parameterFormatter.format(message.params); |
|
576 })); |
|
577 } |
|
578 if (!subDone) { |
|
579 // There were no substitutions in the text, so format the entire params object |
|
580 let rest = this.parameterFormatter.format(message.params); |
|
581 if (rest !== null && rest != "{}") { |
|
582 textParts.push(rest); |
|
583 } |
|
584 } |
|
585 return textParts.join(': '); |
|
586 } |
|
587 }, |
|
588 |
|
589 format: function BF_format(message) { |
|
590 return message.time + "\t" + |
|
591 message.loggerName + "\t" + |
|
592 message.levelDesc + "\t" + |
|
593 this.formatText(message); |
|
594 } |
|
595 }; |
|
596 |
|
597 /** |
|
598 * A formatter that only formats the string message component. |
|
599 */ |
|
600 function MessageOnlyFormatter() { |
|
601 } |
|
602 MessageOnlyFormatter.prototype = Object.freeze({ |
|
603 __proto__: Formatter.prototype, |
|
604 |
|
605 format: function (message) { |
|
606 return message.message; |
|
607 }, |
|
608 }); |
|
609 |
|
610 // Structured formatter that outputs JSON based on message data. |
|
611 // This formatter will format unstructured messages by supplying |
|
612 // default values. |
|
613 function StructuredFormatter() { } |
|
614 StructuredFormatter.prototype = { |
|
615 __proto__: Formatter.prototype, |
|
616 |
|
617 format: function (logMessage) { |
|
618 let output = { |
|
619 _time: logMessage.time, |
|
620 _namespace: logMessage.loggerName, |
|
621 _level: logMessage.levelDesc |
|
622 }; |
|
623 |
|
624 for (let key in logMessage.params) { |
|
625 output[key] = logMessage.params[key]; |
|
626 } |
|
627 |
|
628 if (!output.action) { |
|
629 output.action = "UNKNOWN"; |
|
630 } |
|
631 |
|
632 if (!output._message && logMessage.message) { |
|
633 output._message = logMessage.message; |
|
634 } |
|
635 |
|
636 return JSON.stringify(output); |
|
637 } |
|
638 } |
|
639 |
|
640 /** |
|
641 * Test an object to see if it is a Mozilla JS Error. |
|
642 */ |
|
643 function isError(aObj) { |
|
644 return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj && |
|
645 "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj); |
|
646 }; |
|
647 |
|
648 /* |
|
649 * Parameter Formatters |
|
650 * These massage an object used as a parameter for a LogMessage into |
|
651 * a string representation of the object. |
|
652 */ |
|
653 |
|
654 function ParameterFormatter() { |
|
655 this._name = "ParameterFormatter" |
|
656 } |
|
657 ParameterFormatter.prototype = { |
|
658 format: function(ob) { |
|
659 try { |
|
660 if (ob === undefined) { |
|
661 return "undefined"; |
|
662 } |
|
663 if (ob === null) { |
|
664 return "null"; |
|
665 } |
|
666 // Pass through primitive types and objects that unbox to primitive types. |
|
667 if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") && |
|
668 typeof(ob) != "function") { |
|
669 return ob; |
|
670 } |
|
671 if (ob instanceof Ci.nsIException) { |
|
672 return ob.toString() + " " + Log.stackTrace(ob); |
|
673 } |
|
674 else if (isError(ob)) { |
|
675 return Log._formatError(ob); |
|
676 } |
|
677 // Just JSONify it. Filter out our internal fields and those the caller has |
|
678 // already handled. |
|
679 return JSON.stringify(ob, (key, val) => { |
|
680 if (INTERNAL_FIELDS.has(key)) { |
|
681 return undefined; |
|
682 } |
|
683 return val; |
|
684 }); |
|
685 } |
|
686 catch (e) { |
|
687 dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e)); |
|
688 } |
|
689 // Fancy formatting failed. Just toSource() it - but even this may fail! |
|
690 try { |
|
691 return ob.toSource(); |
|
692 } catch (_) { } |
|
693 try { |
|
694 return "" + ob; |
|
695 } catch (_) { |
|
696 return "[object]" |
|
697 } |
|
698 } |
|
699 } |
|
700 |
|
701 /* |
|
702 * Appenders |
|
703 * These can be attached to Loggers to log to different places |
|
704 * Simply subclass and override doAppend to implement a new one |
|
705 */ |
|
706 |
|
707 function Appender(formatter) { |
|
708 this._name = "Appender"; |
|
709 this._formatter = formatter? formatter : new BasicFormatter(); |
|
710 } |
|
711 Appender.prototype = { |
|
712 level: Log.Level.All, |
|
713 |
|
714 append: function App_append(message) { |
|
715 if (message) { |
|
716 this.doAppend(this._formatter.format(message)); |
|
717 } |
|
718 }, |
|
719 toString: function App_toString() { |
|
720 return this._name + " [level=" + this.level + |
|
721 ", formatter=" + this._formatter + "]"; |
|
722 }, |
|
723 doAppend: function App_doAppend(formatted) {} |
|
724 }; |
|
725 |
|
726 /* |
|
727 * DumpAppender |
|
728 * Logs to standard out |
|
729 */ |
|
730 |
|
731 function DumpAppender(formatter) { |
|
732 Appender.call(this, formatter); |
|
733 this._name = "DumpAppender"; |
|
734 } |
|
735 DumpAppender.prototype = { |
|
736 __proto__: Appender.prototype, |
|
737 |
|
738 doAppend: function DApp_doAppend(formatted) { |
|
739 dump(formatted + "\n"); |
|
740 } |
|
741 }; |
|
742 |
|
743 /* |
|
744 * ConsoleAppender |
|
745 * Logs to the javascript console |
|
746 */ |
|
747 |
|
748 function ConsoleAppender(formatter) { |
|
749 Appender.call(this, formatter); |
|
750 this._name = "ConsoleAppender"; |
|
751 } |
|
752 ConsoleAppender.prototype = { |
|
753 __proto__: Appender.prototype, |
|
754 |
|
755 // XXX this should be replaced with calls to the Browser Console |
|
756 append: function App_append(message) { |
|
757 if (message) { |
|
758 let m = this._formatter.format(message); |
|
759 if (message.level > Log.Level.Warn) { |
|
760 Cu.reportError(m); |
|
761 return; |
|
762 } |
|
763 this.doAppend(m); |
|
764 } |
|
765 }, |
|
766 |
|
767 doAppend: function CApp_doAppend(formatted) { |
|
768 Cc["@mozilla.org/consoleservice;1"]. |
|
769 getService(Ci.nsIConsoleService).logStringMessage(formatted); |
|
770 } |
|
771 }; |
|
772 |
|
773 /** |
|
774 * Append to an nsIStorageStream |
|
775 * |
|
776 * This writes logging output to an in-memory stream which can later be read |
|
777 * back as an nsIInputStream. It can be used to avoid expensive I/O operations |
|
778 * during logging. Instead, one can periodically consume the input stream and |
|
779 * e.g. write it to disk asynchronously. |
|
780 */ |
|
781 function StorageStreamAppender(formatter) { |
|
782 Appender.call(this, formatter); |
|
783 this._name = "StorageStreamAppender"; |
|
784 } |
|
785 |
|
786 StorageStreamAppender.prototype = { |
|
787 __proto__: Appender.prototype, |
|
788 |
|
789 _converterStream: null, // holds the nsIConverterOutputStream |
|
790 _outputStream: null, // holds the underlying nsIOutputStream |
|
791 |
|
792 _ss: null, |
|
793 |
|
794 get outputStream() { |
|
795 if (!this._outputStream) { |
|
796 // First create a raw stream. We can bail out early if that fails. |
|
797 this._outputStream = this.newOutputStream(); |
|
798 if (!this._outputStream) { |
|
799 return null; |
|
800 } |
|
801 |
|
802 // Wrap the raw stream in an nsIConverterOutputStream. We can reuse |
|
803 // the instance if we already have one. |
|
804 if (!this._converterStream) { |
|
805 this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"] |
|
806 .createInstance(Ci.nsIConverterOutputStream); |
|
807 } |
|
808 this._converterStream.init( |
|
809 this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE, |
|
810 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); |
|
811 } |
|
812 return this._converterStream; |
|
813 }, |
|
814 |
|
815 newOutputStream: function newOutputStream() { |
|
816 let ss = this._ss = Cc["@mozilla.org/storagestream;1"] |
|
817 .createInstance(Ci.nsIStorageStream); |
|
818 ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null); |
|
819 return ss.getOutputStream(0); |
|
820 }, |
|
821 |
|
822 getInputStream: function getInputStream() { |
|
823 if (!this._ss) { |
|
824 return null; |
|
825 } |
|
826 return this._ss.newInputStream(0); |
|
827 }, |
|
828 |
|
829 reset: function reset() { |
|
830 if (!this._outputStream) { |
|
831 return; |
|
832 } |
|
833 this.outputStream.close(); |
|
834 this._outputStream = null; |
|
835 this._ss = null; |
|
836 }, |
|
837 |
|
838 doAppend: function (formatted) { |
|
839 if (!formatted) { |
|
840 return; |
|
841 } |
|
842 try { |
|
843 this.outputStream.writeString(formatted + "\n"); |
|
844 } catch(ex) { |
|
845 if (ex.result == Cr.NS_BASE_STREAM_CLOSED) { |
|
846 // The underlying output stream is closed, so let's open a new one |
|
847 // and try again. |
|
848 this._outputStream = null; |
|
849 } try { |
|
850 this.outputStream.writeString(formatted + "\n"); |
|
851 } catch (ex) { |
|
852 // Ah well, we tried, but something seems to be hosed permanently. |
|
853 } |
|
854 } |
|
855 } |
|
856 }; |
|
857 |
|
858 /** |
|
859 * File appender |
|
860 * |
|
861 * Writes output to file using OS.File. |
|
862 */ |
|
863 function FileAppender(path, formatter) { |
|
864 Appender.call(this, formatter); |
|
865 this._name = "FileAppender"; |
|
866 this._encoder = new TextEncoder(); |
|
867 this._path = path; |
|
868 this._file = null; |
|
869 this._fileReadyPromise = null; |
|
870 |
|
871 // This is a promise exposed for testing/debugging the logger itself. |
|
872 this._lastWritePromise = null; |
|
873 } |
|
874 |
|
875 FileAppender.prototype = { |
|
876 __proto__: Appender.prototype, |
|
877 |
|
878 _openFile: function () { |
|
879 return Task.spawn(function _openFile() { |
|
880 try { |
|
881 this._file = yield OS.File.open(this._path, |
|
882 {truncate: true}); |
|
883 } catch (err) { |
|
884 if (err instanceof OS.File.Error) { |
|
885 this._file = null; |
|
886 } else { |
|
887 throw err; |
|
888 } |
|
889 } |
|
890 }.bind(this)); |
|
891 }, |
|
892 |
|
893 _getFile: function() { |
|
894 if (!this._fileReadyPromise) { |
|
895 this._fileReadyPromise = this._openFile(); |
|
896 return this._fileReadyPromise; |
|
897 } |
|
898 |
|
899 return this._fileReadyPromise.then(_ => { |
|
900 if (!this._file) { |
|
901 return this._openFile(); |
|
902 } |
|
903 }); |
|
904 }, |
|
905 |
|
906 doAppend: function (formatted) { |
|
907 let array = this._encoder.encode(formatted + "\n"); |
|
908 if (this._file) { |
|
909 this._lastWritePromise = this._file.write(array); |
|
910 } else { |
|
911 this._lastWritePromise = this._getFile().then(_ => { |
|
912 this._fileReadyPromise = null; |
|
913 if (this._file) { |
|
914 return this._file.write(array); |
|
915 } |
|
916 }); |
|
917 } |
|
918 }, |
|
919 |
|
920 reset: function () { |
|
921 let fileClosePromise = this._file.close(); |
|
922 return fileClosePromise.then(_ => { |
|
923 this._file = null; |
|
924 return OS.File.remove(this._path); |
|
925 }); |
|
926 } |
|
927 }; |
|
928 |
|
929 /** |
|
930 * Bounded File appender |
|
931 * |
|
932 * Writes output to file using OS.File. After the total message size |
|
933 * (as defined by formatted.length) exceeds maxSize, existing messages |
|
934 * will be discarded, and subsequent writes will be appended to a new log file. |
|
935 */ |
|
936 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) { |
|
937 FileAppender.call(this, path, formatter); |
|
938 this._name = "BoundedFileAppender"; |
|
939 this._size = 0; |
|
940 this._maxSize = maxSize; |
|
941 this._closeFilePromise = null; |
|
942 } |
|
943 |
|
944 BoundedFileAppender.prototype = { |
|
945 __proto__: FileAppender.prototype, |
|
946 |
|
947 doAppend: function (formatted) { |
|
948 if (!this._removeFilePromise) { |
|
949 if (this._size < this._maxSize) { |
|
950 this._size += formatted.length; |
|
951 return FileAppender.prototype.doAppend.call(this, formatted); |
|
952 } |
|
953 this._removeFilePromise = this.reset(); |
|
954 } |
|
955 this._removeFilePromise.then(_ => { |
|
956 this._removeFilePromise = null; |
|
957 this.doAppend(formatted); |
|
958 }); |
|
959 }, |
|
960 |
|
961 reset: function () { |
|
962 let fileClosePromise; |
|
963 if (this._fileReadyPromise) { |
|
964 // An attempt to open the file may still be in progress. |
|
965 fileClosePromise = this._fileReadyPromise.then(_ => { |
|
966 return this._file.close(); |
|
967 }); |
|
968 } else { |
|
969 fileClosePromise = this._file.close(); |
|
970 } |
|
971 |
|
972 return fileClosePromise.then(_ => { |
|
973 this._size = 0; |
|
974 this._file = null; |
|
975 return OS.File.remove(this._path); |
|
976 }); |
|
977 } |
|
978 }; |
|
979 |