Bug 948389 - Replace mozilla-banner.gif with a plain blue image in 405577-1.html...
[gecko.git] / toolkit / modules / Log.jsm
blob55d6a84111698dfa69a1a9893712a9fb2eaedb53
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/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["Log"];
9 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
11 const ONE_BYTE = 1;
12 const ONE_KILOBYTE = 1024 * ONE_BYTE;
13 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
15 const STREAM_SEGMENT_SIZE = 4096;
16 const PR_UINT32_MAX = 0xffffffff;
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");
24 this.Log = {
25   Level: {
26     Fatal:  70,
27     Error:  60,
28     Warn:   50,
29     Info:   40,
30     Config: 30,
31     Debug:  20,
32     Trace:  10,
33     All:    0,
34     Desc: {
35       70: "FATAL",
36       60: "ERROR",
37       50: "WARN",
38       40: "INFO",
39       30: "CONFIG",
40       20: "DEBUG",
41       10: "TRACE",
42       0:  "ALL"
43     },
44     Numbers: {
45       "FATAL": 70,
46       "ERROR": 60,
47       "WARN": 50,
48       "INFO": 40,
49       "CONFIG": 30,
50       "DEBUG": 20,
51       "TRACE": 10,
52       "ALL": 0,
53     }
54   },
56   get repository() {
57     delete Log.repository;
58     Log.repository = new LoggerRepository();
59     return Log.repository;
60   },
61   set repository(value) {
62     delete Log.repository;
63     Log.repository = value;
64   },
66   LogMessage: LogMessage,
67   Logger: Logger,
68   LoggerRepository: LoggerRepository,
70   Formatter: Formatter,
71   BasicFormatter: BasicFormatter,
72   StructuredFormatter: StructuredFormatter,
74   Appender: Appender,
75   DumpAppender: DumpAppender,
76   ConsoleAppender: ConsoleAppender,
77   StorageStreamAppender: StorageStreamAppender,
79   FileAppender: FileAppender,
80   BoundedFileAppender: BoundedFileAppender,
82   // Logging helper:
83   // let logger = Log.repository.getLogger("foo");
84   // logger.info(Log.enumerateInterfaces(someObject).join(","));
85   enumerateInterfaces: function Log_enumerateInterfaces(aObject) {
86     let interfaces = [];
88     for (i in Ci) {
89       try {
90         aObject.QueryInterface(Ci[i]);
91         interfaces.push(i);
92       }
93       catch(ex) {}
94     }
96     return interfaces;
97   },
99   // Logging helper:
100   // let logger = Log.repository.getLogger("foo");
101   // logger.info(Log.enumerateProperties(someObject).join(","));
102   enumerateProperties: function Log_enumerateProps(aObject,
103                                                        aExcludeComplexTypes) {
104     let properties = [];
106     for (p in aObject) {
107       try {
108         if (aExcludeComplexTypes &&
109             (typeof aObject[p] == "object" || typeof aObject[p] == "function"))
110           continue;
111         properties.push(p + " = " + aObject[p]);
112       }
113       catch(ex) {
114         properties.push(p + " = " + ex);
115       }
116     }
118     return properties;
119   }
124  * LogMessage
125  * Encapsulates a single log event's data
126  */
127 function LogMessage(loggerName, level, message, params) {
128   this.loggerName = loggerName;
129   this.level = level;
130   this.message = message;
131   this.params = params;
133   // The _structured field will correspond to whether this message is to
134   // be interpreted as a structured message.
135   this._structured = this.params && this.params.action;
136   this.time = Date.now();
138 LogMessage.prototype = {
139   get levelDesc() {
140     if (this.level in Log.Level.Desc)
141       return Log.Level.Desc[this.level];
142     return "UNKNOWN";
143   },
145   toString: function LogMsg_toString(){
146     let msg = "LogMessage [" + this.time + " " + this.level + " " +
147       this.message;
148     if (this.params) {
149       msg += " " + JSON.stringify(this.params);
150     }
151     return msg + "]"
152   }
156  * Logger
157  * Hierarchical version.  Logs to all appenders, assigned or inherited
158  */
160 function Logger(name, repository) {
161   if (!repository)
162     repository = Log.repository;
163   this._name = name;
164   this.children = [];
165   this.ownAppenders = [];
166   this.appenders = [];
167   this._repository = repository;
169 Logger.prototype = {
170   get name() {
171     return this._name;
172   },
174   _level: null,
175   get level() {
176     if (this._level != null)
177       return this._level;
178     if (this.parent)
179       return this.parent.level;
180     dump("Log warning: root logger configuration error: no level defined\n");
181     return Log.Level.All;
182   },
183   set level(level) {
184     this._level = level;
185   },
187   _parent: null,
188   get parent() this._parent,
189   set parent(parent) {
190     if (this._parent == parent) {
191       return;
192     }
193     // Remove ourselves from parent's children
194     if (this._parent) {
195       let index = this._parent.children.indexOf(this);
196       if (index != -1) {
197         this._parent.children.splice(index, 1);
198       }
199     }
200     this._parent = parent;
201     parent.children.push(this);
202     this.updateAppenders();
203   },
205   updateAppenders: function updateAppenders() {
206     if (this._parent) {
207       let notOwnAppenders = this._parent.appenders.filter(function(appender) {
208         return this.ownAppenders.indexOf(appender) == -1;
209       }, this);
210       this.appenders = notOwnAppenders.concat(this.ownAppenders);
211     } else {
212       this.appenders = this.ownAppenders.slice();
213     }
215     // Update children's appenders.
216     for (let i = 0; i < this.children.length; i++) {
217       this.children[i].updateAppenders();
218     }
219   },
221   addAppender: function Logger_addAppender(appender) {
222     if (this.ownAppenders.indexOf(appender) != -1) {
223       return;
224     }
225     this.ownAppenders.push(appender);
226     this.updateAppenders();
227   },
229   removeAppender: function Logger_removeAppender(appender) {
230     let index = this.ownAppenders.indexOf(appender);
231     if (index == -1) {
232       return;
233     }
234     this.ownAppenders.splice(index, 1);
235     this.updateAppenders();
236   },
238   /**
239    * Logs a structured message object.
240    *
241    * @param action
242    *        (string) A message action, one of a set of actions known to the
243    *          log consumer.
244    * @param params
245    *        (object) Parameters to be included in the message.
246    *          If _level is included as a key and the corresponding value
247    *          is a number or known level name, the message will be logged
248    *          at the indicated level.
249    */
250   logStructured: function (action, params) {
251     if (!action) {
252       throw "An action is required when logging a structured message.";
253     }
254     if (!params) {
255       return this.log(this.level, undefined, {"action": action});
256     }
257     if (typeof params != "object") {
258       throw "The params argument is required to be an object.";
259     }
261     let level = params._level || this.level;
262     if ((typeof level == "string") && level in Log.Level.Numbers) {
263       level = Log.Level.Numbers[level];
264     }
266     params.action = action;
267     this.log(level, params._message, params);
268   },
270   log: function (level, string, params) {
271     if (this.level > level)
272       return;
274     // Hold off on creating the message object until we actually have
275     // an appender that's responsible.
276     let message;
277     let appenders = this.appenders;
278     for (let appender of appenders) {
279       if (appender.level > level) {
280         continue;
281       }
282       if (!message) {
283         message = new LogMessage(this._name, level, string, params);
284       }
285       appender.append(message);
286     }
287   },
289   fatal: function (string, params) {
290     this.log(Log.Level.Fatal, string, params);
291   },
292   error: function (string, params) {
293     this.log(Log.Level.Error, string, params);
294   },
295   warn: function (string, params) {
296     this.log(Log.Level.Warn, string, params);
297   },
298   info: function (string, params) {
299     this.log(Log.Level.Info, string, params);
300   },
301   config: function (string, params) {
302     this.log(Log.Level.Config, string, params);
303   },
304   debug: function (string, params) {
305     this.log(Log.Level.Debug, string, params);
306   },
307   trace: function (string, params) {
308     this.log(Log.Level.Trace, string, params);
309   }
313  * LoggerRepository
314  * Implements a hierarchy of Loggers
315  */
317 function LoggerRepository() {}
318 LoggerRepository.prototype = {
319   _loggers: {},
321   _rootLogger: null,
322   get rootLogger() {
323     if (!this._rootLogger) {
324       this._rootLogger = new Logger("root", this);
325       this._rootLogger.level = Log.Level.All;
326     }
327     return this._rootLogger;
328   },
329   set rootLogger(logger) {
330     throw "Cannot change the root logger";
331   },
333   _updateParents: function LogRep__updateParents(name) {
334     let pieces = name.split('.');
335     let cur, parent;
337     // find the closest parent
338     // don't test for the logger name itself, as there's a chance it's already
339     // there in this._loggers
340     for (let i = 0; i < pieces.length - 1; i++) {
341       if (cur)
342         cur += '.' + pieces[i];
343       else
344         cur = pieces[i];
345       if (cur in this._loggers)
346         parent = cur;
347     }
349     // if we didn't assign a parent above, there is no parent
350     if (!parent)
351       this._loggers[name].parent = this.rootLogger;
352     else
353       this._loggers[name].parent = this._loggers[parent];
355     // trigger updates for any possible descendants of this logger
356     for (let logger in this._loggers) {
357       if (logger != name && logger.indexOf(name) == 0)
358         this._updateParents(logger);
359     }
360   },
362   getLogger: function LogRep_getLogger(name) {
363     if (name in this._loggers)
364       return this._loggers[name];
365     this._loggers[name] = new Logger(name, this);
366     this._updateParents(name);
367     return this._loggers[name];
368   }
372  * Formatters
373  * These massage a LogMessage into whatever output is desired.
374  * BasicFormatter and StructuredFormatter are implemented here.
375  */
377 // Abstract formatter
378 function Formatter() {}
379 Formatter.prototype = {
380   format: function Formatter_format(message) {}
383 // Basic formatter that doesn't do anything fancy.
384 function BasicFormatter(dateFormat) {
385   if (dateFormat)
386     this.dateFormat = dateFormat;
388 BasicFormatter.prototype = {
389   __proto__: Formatter.prototype,
391   format: function BF_format(message) {
392     return message.time + "\t" +
393       message.loggerName + "\t" +
394       message.levelDesc + "\t" +
395       message.message + "\n";
396   }
399 // Structured formatter that outputs JSON based on message data.
400 // This formatter will format unstructured messages by supplying
401 // default values.
402 function StructuredFormatter() { }
403 StructuredFormatter.prototype = {
404   __proto__: Formatter.prototype,
406   format: function (logMessage) {
407     let output = {
408       _time: logMessage.time,
409       _namespace: logMessage.loggerName,
410       _level: logMessage.levelDesc
411     };
413     for (let key in logMessage.params) {
414       output[key] = logMessage.params[key];
415     }
417     if (!output.action) {
418       output.action = "UNKNOWN";
419     }
421     if (!output._message && logMessage.message) {
422       output._message = logMessage.message;
423     }
425     return JSON.stringify(output);
426   }
430  * Appenders
431  * These can be attached to Loggers to log to different places
432  * Simply subclass and override doAppend to implement a new one
433  */
435 function Appender(formatter) {
436   this._name = "Appender";
437   this._formatter = formatter? formatter : new BasicFormatter();
439 Appender.prototype = {
440   level: Log.Level.All,
442   append: function App_append(message) {
443     if (message) {
444       this.doAppend(this._formatter.format(message));
445     }
446   },
447   toString: function App_toString() {
448     return this._name + " [level=" + this._level +
449       ", formatter=" + this._formatter + "]";
450   },
451   doAppend: function App_doAppend(message) {}
455  * DumpAppender
456  * Logs to standard out
457  */
459 function DumpAppender(formatter) {
460   this._name = "DumpAppender";
461   Appender.call(this, formatter);
463 DumpAppender.prototype = {
464   __proto__: Appender.prototype,
466   doAppend: function DApp_doAppend(message) {
467     dump(message);
468   }
472  * ConsoleAppender
473  * Logs to the javascript console
474  */
476 function ConsoleAppender(formatter) {
477   this._name = "ConsoleAppender";
478   Appender.call(this, formatter);
480 ConsoleAppender.prototype = {
481   __proto__: Appender.prototype,
483   doAppend: function CApp_doAppend(message) {
484     if (message.level > Log.Level.Warn) {
485       Cu.reportError(message);
486       return;
487     }
488     Cc["@mozilla.org/consoleservice;1"].
489       getService(Ci.nsIConsoleService).logStringMessage(message);
490   }
494  * Append to an nsIStorageStream
496  * This writes logging output to an in-memory stream which can later be read
497  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
498  * during logging. Instead, one can periodically consume the input stream and
499  * e.g. write it to disk asynchronously.
500  */
501 function StorageStreamAppender(formatter) {
502   this._name = "StorageStreamAppender";
503   Appender.call(this, formatter);
506 StorageStreamAppender.prototype = {
507   __proto__: Appender.prototype,
509   _converterStream: null, // holds the nsIConverterOutputStream
510   _outputStream: null,    // holds the underlying nsIOutputStream
512   _ss: null,
514   get outputStream() {
515     if (!this._outputStream) {
516       // First create a raw stream. We can bail out early if that fails.
517       this._outputStream = this.newOutputStream();
518       if (!this._outputStream) {
519         return null;
520       }
522       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
523       // the instance if we already have one.
524       if (!this._converterStream) {
525         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
526                                   .createInstance(Ci.nsIConverterOutputStream);
527       }
528       this._converterStream.init(
529         this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
530         Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
531     }
532     return this._converterStream;
533   },
535   newOutputStream: function newOutputStream() {
536     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
537                           .createInstance(Ci.nsIStorageStream);
538     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
539     return ss.getOutputStream(0);
540   },
542   getInputStream: function getInputStream() {
543     if (!this._ss) {
544       return null;
545     }
546     return this._ss.newInputStream(0);
547   },
549   reset: function reset() {
550     if (!this._outputStream) {
551       return;
552     }
553     this.outputStream.close();
554     this._outputStream = null;
555     this._ss = null;
556   },
558   doAppend: function (message) {
559     if (!message) {
560       return;
561     }
562     try {
563       this.outputStream.writeString(message);
564     } catch(ex) {
565       if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
566         // The underlying output stream is closed, so let's open a new one
567         // and try again.
568         this._outputStream = null;
569       } try {
570           this.outputStream.writeString(message);
571       } catch (ex) {
572         // Ah well, we tried, but something seems to be hosed permanently.
573       }
574     }
575   }
579  * File appender
581  * Writes output to file using OS.File.
582  */
583 function FileAppender(path, formatter) {
584   this._name = "FileAppender";
585   this._encoder = new TextEncoder();
586   this._path = path;
587   this._file = null;
588   this._fileReadyPromise = null;
590   // This is a promise exposed for testing/debugging the logger itself.
591   this._lastWritePromise = null;
592   Appender.call(this, formatter);
595 FileAppender.prototype = {
596   __proto__: Appender.prototype,
598   _openFile: function () {
599     return Task.spawn(function _openFile() {
600       try {
601         this._file = yield OS.File.open(this._path,
602                                         {truncate: true});
603       } catch (err) {
604         if (err instanceof OS.File.Error) {
605           this._file = null;
606         } else {
607           throw err;
608         }
609       }
610     }.bind(this));
611   },
613   _getFile: function() {
614     if (!this._fileReadyPromise) {
615       this._fileReadyPromise = this._openFile();
616       return this._fileReadyPromise;
617     }
619     return this._fileReadyPromise.then(_ => {
620       if (!this._file) {
621         return this._openFile();
622       }
623     });
624   },
626   doAppend: function (message) {
627     let array = this._encoder.encode(message);
628     if (this._file) {
629       this._lastWritePromise = this._file.write(array);
630     } else {
631       this._lastWritePromise = this._getFile().then(_ => {
632         this._fileReadyPromise = null;
633         if (this._file) {
634           return this._file.write(array);
635         }
636       });
637     }
638   },
640   reset: function () {
641     let fileClosePromise = this._file.close();
642     return fileClosePromise.then(_ => {
643       this._file = null;
644       return OS.File.remove(this._path);
645     });
646   }
650  * Bounded File appender
652  * Writes output to file using OS.File. After the total message size
653  * (as defined by message.length) exceeds maxSize, existing messages
654  * will be discarded, and subsequent writes will be appended to a new log file.
655  */
656 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
657   this._name = "BoundedFileAppender";
658   this._size = 0;
659   this._maxSize = maxSize;
660   this._closeFilePromise = null;
661   FileAppender.call(this, path, formatter);
664 BoundedFileAppender.prototype = {
665   __proto__: FileAppender.prototype,
667   doAppend: function (message) {
668     if (!this._removeFilePromise) {
669       if (this._size < this._maxSize) {
670         this._size += message.length;
671         return FileAppender.prototype.doAppend.call(this, message);
672       }
673       this._removeFilePromise = this.reset();
674     }
675     this._removeFilePromise.then(_ => {
676       this._removeFilePromise = null;
677       this.doAppend(message);
678     });
679   },
681   reset: function () {
682     let fileClosePromise;
683     if (this._fileReadyPromise) {
684       // An attempt to open the file may still be in progress.
685       fileClosePromise = this._fileReadyPromise.then(_ => {
686         return this._file.close();
687       });
688     } else {
689       fileClosePromise = this._file.close();
690     }
692     return fileClosePromise.then(_ => {
693       this._size = 0;
694       this._file = null;
695       return OS.File.remove(this._path);
696     });
697   }