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/. */
7 this.EXPORTED_SYMBOLS = ["Log"];
9 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
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");
57 delete Log.repository;
58 Log.repository = new LoggerRepository();
59 return Log.repository;
61 set repository(value) {
62 delete Log.repository;
63 Log.repository = value;
66 LogMessage: LogMessage,
68 LoggerRepository: LoggerRepository,
71 BasicFormatter: BasicFormatter,
72 StructuredFormatter: StructuredFormatter,
75 DumpAppender: DumpAppender,
76 ConsoleAppender: ConsoleAppender,
77 StorageStreamAppender: StorageStreamAppender,
79 FileAppender: FileAppender,
80 BoundedFileAppender: BoundedFileAppender,
83 // let logger = Log.repository.getLogger("foo");
84 // logger.info(Log.enumerateInterfaces(someObject).join(","));
85 enumerateInterfaces: function Log_enumerateInterfaces(aObject) {
90 aObject.QueryInterface(Ci[i]);
100 // let logger = Log.repository.getLogger("foo");
101 // logger.info(Log.enumerateProperties(someObject).join(","));
102 enumerateProperties: function Log_enumerateProps(aObject,
103 aExcludeComplexTypes) {
108 if (aExcludeComplexTypes &&
109 (typeof aObject[p] == "object" || typeof aObject[p] == "function"))
111 properties.push(p + " = " + aObject[p]);
114 properties.push(p + " = " + ex);
125 * Encapsulates a single log event's data
127 function LogMessage(loggerName, level, message, params) {
128 this.loggerName = loggerName;
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 = {
140 if (this.level in Log.Level.Desc)
141 return Log.Level.Desc[this.level];
145 toString: function LogMsg_toString(){
146 let msg = "LogMessage [" + this.time + " " + this.level + " " +
149 msg += " " + JSON.stringify(this.params);
157 * Hierarchical version. Logs to all appenders, assigned or inherited
160 function Logger(name, repository) {
162 repository = Log.repository;
165 this.ownAppenders = [];
167 this._repository = repository;
176 if (this._level != null)
179 return this.parent.level;
180 dump("Log warning: root logger configuration error: no level defined\n");
181 return Log.Level.All;
188 get parent() this._parent,
190 if (this._parent == parent) {
193 // Remove ourselves from parent's children
195 let index = this._parent.children.indexOf(this);
197 this._parent.children.splice(index, 1);
200 this._parent = parent;
201 parent.children.push(this);
202 this.updateAppenders();
205 updateAppenders: function updateAppenders() {
207 let notOwnAppenders = this._parent.appenders.filter(function(appender) {
208 return this.ownAppenders.indexOf(appender) == -1;
210 this.appenders = notOwnAppenders.concat(this.ownAppenders);
212 this.appenders = this.ownAppenders.slice();
215 // Update children's appenders.
216 for (let i = 0; i < this.children.length; i++) {
217 this.children[i].updateAppenders();
221 addAppender: function Logger_addAppender(appender) {
222 if (this.ownAppenders.indexOf(appender) != -1) {
225 this.ownAppenders.push(appender);
226 this.updateAppenders();
229 removeAppender: function Logger_removeAppender(appender) {
230 let index = this.ownAppenders.indexOf(appender);
234 this.ownAppenders.splice(index, 1);
235 this.updateAppenders();
239 * Logs a structured message object.
242 * (string) A message action, one of a set of actions known to the
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.
250 logStructured: function (action, params) {
252 throw "An action is required when logging a structured message.";
255 return this.log(this.level, undefined, {"action": action});
257 if (typeof params != "object") {
258 throw "The params argument is required to be an object.";
261 let level = params._level || this.level;
262 if ((typeof level == "string") && level in Log.Level.Numbers) {
263 level = Log.Level.Numbers[level];
266 params.action = action;
267 this.log(level, params._message, params);
270 log: function (level, string, params) {
271 if (this.level > level)
274 // Hold off on creating the message object until we actually have
275 // an appender that's responsible.
277 let appenders = this.appenders;
278 for (let appender of appenders) {
279 if (appender.level > level) {
283 message = new LogMessage(this._name, level, string, params);
285 appender.append(message);
289 fatal: function (string, params) {
290 this.log(Log.Level.Fatal, string, params);
292 error: function (string, params) {
293 this.log(Log.Level.Error, string, params);
295 warn: function (string, params) {
296 this.log(Log.Level.Warn, string, params);
298 info: function (string, params) {
299 this.log(Log.Level.Info, string, params);
301 config: function (string, params) {
302 this.log(Log.Level.Config, string, params);
304 debug: function (string, params) {
305 this.log(Log.Level.Debug, string, params);
307 trace: function (string, params) {
308 this.log(Log.Level.Trace, string, params);
314 * Implements a hierarchy of Loggers
317 function LoggerRepository() {}
318 LoggerRepository.prototype = {
323 if (!this._rootLogger) {
324 this._rootLogger = new Logger("root", this);
325 this._rootLogger.level = Log.Level.All;
327 return this._rootLogger;
329 set rootLogger(logger) {
330 throw "Cannot change the root logger";
333 _updateParents: function LogRep__updateParents(name) {
334 let pieces = name.split('.');
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++) {
342 cur += '.' + pieces[i];
345 if (cur in this._loggers)
349 // if we didn't assign a parent above, there is no parent
351 this._loggers[name].parent = this.rootLogger;
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);
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];
373 * These massage a LogMessage into whatever output is desired.
374 * BasicFormatter and StructuredFormatter are implemented here.
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) {
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";
399 // Structured formatter that outputs JSON based on message data.
400 // This formatter will format unstructured messages by supplying
402 function StructuredFormatter() { }
403 StructuredFormatter.prototype = {
404 __proto__: Formatter.prototype,
406 format: function (logMessage) {
408 _time: logMessage.time,
409 _namespace: logMessage.loggerName,
410 _level: logMessage.levelDesc
413 for (let key in logMessage.params) {
414 output[key] = logMessage.params[key];
417 if (!output.action) {
418 output.action = "UNKNOWN";
421 if (!output._message && logMessage.message) {
422 output._message = logMessage.message;
425 return JSON.stringify(output);
431 * These can be attached to Loggers to log to different places
432 * Simply subclass and override doAppend to implement a new one
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) {
444 this.doAppend(this._formatter.format(message));
447 toString: function App_toString() {
448 return this._name + " [level=" + this._level +
449 ", formatter=" + this._formatter + "]";
451 doAppend: function App_doAppend(message) {}
456 * Logs to standard out
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) {
473 * Logs to the javascript console
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);
488 Cc["@mozilla.org/consoleservice;1"].
489 getService(Ci.nsIConsoleService).logStringMessage(message);
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.
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
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) {
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);
528 this._converterStream.init(
529 this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
530 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
532 return this._converterStream;
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);
542 getInputStream: function getInputStream() {
546 return this._ss.newInputStream(0);
549 reset: function reset() {
550 if (!this._outputStream) {
553 this.outputStream.close();
554 this._outputStream = null;
558 doAppend: function (message) {
563 this.outputStream.writeString(message);
565 if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
566 // The underlying output stream is closed, so let's open a new one
568 this._outputStream = null;
570 this.outputStream.writeString(message);
572 // Ah well, we tried, but something seems to be hosed permanently.
581 * Writes output to file using OS.File.
583 function FileAppender(path, formatter) {
584 this._name = "FileAppender";
585 this._encoder = new TextEncoder();
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() {
601 this._file = yield OS.File.open(this._path,
604 if (err instanceof OS.File.Error) {
613 _getFile: function() {
614 if (!this._fileReadyPromise) {
615 this._fileReadyPromise = this._openFile();
616 return this._fileReadyPromise;
619 return this._fileReadyPromise.then(_ => {
621 return this._openFile();
626 doAppend: function (message) {
627 let array = this._encoder.encode(message);
629 this._lastWritePromise = this._file.write(array);
631 this._lastWritePromise = this._getFile().then(_ => {
632 this._fileReadyPromise = null;
634 return this._file.write(array);
641 let fileClosePromise = this._file.close();
642 return fileClosePromise.then(_ => {
644 return OS.File.remove(this._path);
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.
656 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
657 this._name = "BoundedFileAppender";
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);
673 this._removeFilePromise = this.reset();
675 this._removeFilePromise.then(_ => {
676 this._removeFilePromise = null;
677 this.doAppend(message);
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();
689 fileClosePromise = this._file.close();
692 return fileClosePromise.then(_ => {
695 return OS.File.remove(this._path);