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");
23 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
27 * Dump a message everywhere we can if we have a failure.
29 function dumpError(text) {
67 delete Log.repository;
68 Log.repository = new LoggerRepository();
69 return Log.repository;
71 set repository(value) {
72 delete Log.repository;
73 Log.repository = value;
76 LogMessage: LogMessage,
78 LoggerRepository: LoggerRepository,
81 BasicFormatter: BasicFormatter,
82 MessageOnlyFormatter: MessageOnlyFormatter,
83 StructuredFormatter: StructuredFormatter,
86 DumpAppender: DumpAppender,
87 ConsoleAppender: ConsoleAppender,
88 StorageStreamAppender: StorageStreamAppender,
90 FileAppender: FileAppender,
91 BoundedFileAppender: BoundedFileAppender,
93 ParameterFormatter: ParameterFormatter,
95 // let logger = Log.repository.getLogger("foo");
96 // logger.info(Log.enumerateInterfaces(someObject).join(","));
97 enumerateInterfaces: function Log_enumerateInterfaces(aObject) {
102 aObject.QueryInterface(Ci[i]);
112 // let logger = Log.repository.getLogger("foo");
113 // logger.info(Log.enumerateProperties(someObject).join(","));
114 enumerateProperties: function (aObject, aExcludeComplexTypes) {
119 if (aExcludeComplexTypes &&
120 (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function"))
122 properties.push(p + " = " + aObject[p]);
125 properties.push(p + " = " + ex);
132 _formatError: function _formatError(e) {
133 let result = e.toString();
135 result += " (" + e.fileName;
137 result += ":" + e.lineNumber;
139 if (e.columnNumber) {
140 result += ":" + e.columnNumber;
144 return result + " " + Log.stackTrace(e);
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) {
153 if (e instanceof Ci.nsIException) {
154 return e.toString() + " " + Log.stackTrace(e);
156 else if (isError(e)) {
157 return Log._formatError(e);
160 let message = e.message ? e.message : e;
161 return message + " " + Log.stackTrace(e);
164 stackTrace: function stackTrace(e) {
165 // Wrapped nsIException
167 let frame = e.location;
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>";
175 let file = frame.filename || frame.fileName;
177 str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
180 if (frame.lineNumber) {
181 str += ":" + frame.lineNumber;
185 str = frame.name + "()@" + str;
191 frame = frame.caller;
193 return "Stack trace: " + output.join(" < ");
195 // Standard JS exception
197 return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
198 replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
201 return "No traceback available";
207 * Encapsulates a single log event's data
209 function LogMessage(loggerName, level, message, params) {
210 this.loggerName = loggerName;
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.
218 if (!params && message && (typeof(message) == "object") &&
219 (typeof(message.valueOf()) != "string")) {
221 this.params = message;
223 // If the message text is empty, or a string, or a String object, normal handling
224 this.message = message;
225 this.params = params;
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();
233 LogMessage.prototype = {
235 if (this.level in Log.Level.Desc)
236 return Log.Level.Desc[this.level];
240 toString: function LogMsg_toString() {
241 let msg = "LogMessage [" + this.time + " " + this.level + " " +
244 msg += " " + JSON.stringify(this.params);
252 * Hierarchical version. Logs to all appenders, assigned or inherited
255 function Logger(name, repository) {
257 repository = Log.repository;
260 this.ownAppenders = [];
262 this._repository = repository;
271 if (this._level != null)
274 return this.parent.level;
275 dumpError("Log warning: root logger configuration error: no level defined");
276 return Log.Level.All;
283 get parent() this._parent,
285 if (this._parent == parent) {
288 // Remove ourselves from parent's children
290 let index = this._parent.children.indexOf(this);
292 this._parent.children.splice(index, 1);
295 this._parent = parent;
296 parent.children.push(this);
297 this.updateAppenders();
300 updateAppenders: function updateAppenders() {
302 let notOwnAppenders = this._parent.appenders.filter(function(appender) {
303 return this.ownAppenders.indexOf(appender) == -1;
305 this.appenders = notOwnAppenders.concat(this.ownAppenders);
307 this.appenders = this.ownAppenders.slice();
310 // Update children's appenders.
311 for (let i = 0; i < this.children.length; i++) {
312 this.children[i].updateAppenders();
316 addAppender: function Logger_addAppender(appender) {
317 if (this.ownAppenders.indexOf(appender) != -1) {
320 this.ownAppenders.push(appender);
321 this.updateAppenders();
324 removeAppender: function Logger_removeAppender(appender) {
325 let index = this.ownAppenders.indexOf(appender);
329 this.ownAppenders.splice(index, 1);
330 this.updateAppenders();
334 * Logs a structured message object.
337 * (string) A message action, one of a set of actions known to the
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.
346 logStructured: function (action, params) {
348 throw "An action is required when logging a structured message.";
351 return this.log(this.level, undefined, {"action": action});
353 if (typeof(params) != "object") {
354 throw "The params argument is required to be an object.";
357 let level = params._level;
359 let ulevel = level.toUpperCase();
360 if (ulevel in Log.Level.Numbers) {
361 level = Log.Level.Numbers[ulevel];
367 params.action = action;
368 this.log(level, params._message, params);
371 log: function (level, string, params) {
372 if (this.level > level)
375 // Hold off on creating the message object until we actually have
376 // an appender that's responsible.
378 let appenders = this.appenders;
379 for (let appender of appenders) {
380 if (appender.level > level) {
384 message = new LogMessage(this._name, level, string, params);
386 appender.append(message);
390 fatal: function (string, params) {
391 this.log(Log.Level.Fatal, string, params);
393 error: function (string, params) {
394 this.log(Log.Level.Error, string, params);
396 warn: function (string, params) {
397 this.log(Log.Level.Warn, string, params);
399 info: function (string, params) {
400 this.log(Log.Level.Info, string, params);
402 config: function (string, params) {
403 this.log(Log.Level.Config, string, params);
405 debug: function (string, params) {
406 this.log(Log.Level.Debug, string, params);
408 trace: function (string, params) {
409 this.log(Log.Level.Trace, string, params);
415 * Implements a hierarchy of Loggers
418 function LoggerRepository() {}
419 LoggerRepository.prototype = {
424 if (!this._rootLogger) {
425 this._rootLogger = new Logger("root", this);
426 this._rootLogger.level = Log.Level.All;
428 return this._rootLogger;
430 set rootLogger(logger) {
431 throw "Cannot change the root logger";
434 _updateParents: function LogRep__updateParents(name) {
435 let pieces = name.split('.');
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++) {
443 cur += '.' + pieces[i];
446 if (cur in this._loggers)
450 // if we didn't assign a parent above, there is no parent
452 this._loggers[name].parent = this.rootLogger;
454 this._loggers[name].parent = this._loggers[parent];
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);
464 * Obtain a named Logger.
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.
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];
481 * Obtain a Logger that logs all string messages with a prefix.
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.
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.
493 * (string) The Logger to retrieve.
495 * (string) The string to prefix each logged message with.
497 getLoggerWithMessagePrefix: function (name, prefix) {
498 let log = this.getLogger(name);
500 let proxy = Object.create(log);
501 proxy.log = (level, string, params) => log.log(level, prefix + string, params);
508 * These massage a LogMessage into whatever output is desired.
509 * BasicFormatter and StructuredFormatter are implemented here.
512 // Abstract formatter
513 function Formatter() {}
514 Formatter.prototype = {
515 format: function Formatter_format(message) {}
518 // Basic formatter that doesn't do anything fancy.
519 function BasicFormatter(dateFormat) {
521 this.dateFormat = dateFormat;
523 this.parameterFormatter = new ParameterFormatter();
525 BasicFormatter.prototype = {
526 __proto__: Formatter.prototype,
529 * Format the text of a message with optional parameters.
530 * If the text contains ${identifier}, replace that with
531 * the value of params[identifier]; if ${}, replace that with
532 * the entire params object. If no params have been substituted
533 * into the text, format the entire object and append that
536 formatText: function (message) {
537 let params = message.params;
539 return message.message || "";
541 // Defensive handling of non-object params
542 // We could add a special case for NSRESULT values here...
543 let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function');
545 // if we have params, try and find substitutions.
546 if (message.params && this.parameterFormatter) {
547 // have we successfully substituted any parameters into the message?
548 // in the log message
550 let regex = /\$\{(\S*)\}/g;
552 if (message.message) {
553 textParts.push(message.message.replace(regex, (_, sub) => {
554 // ${foo} means use the params['foo']
556 if (pIsObject && sub in message.params) {
558 return this.parameterFormatter.format(message.params[sub]);
560 return '${' + sub + '}';
562 // ${} means use the entire params object.
564 return this.parameterFormatter.format(message.params);
568 // There were no substitutions in the text, so format the entire params object
569 let rest = this.parameterFormatter.format(message.params);
570 if (rest !== null && rest != "{}") {
571 textParts.push(rest);
574 return textParts.join(': ');
578 format: function BF_format(message) {
579 return message.time + "\t" +
580 message.loggerName + "\t" +
581 message.levelDesc + "\t" +
582 this.formatText(message);
587 * A formatter that only formats the string message component.
589 function MessageOnlyFormatter() {
591 MessageOnlyFormatter.prototype = Object.freeze({
592 __proto__: Formatter.prototype,
594 format: function (message) {
595 return message.message;
599 // Structured formatter that outputs JSON based on message data.
600 // This formatter will format unstructured messages by supplying
602 function StructuredFormatter() { }
603 StructuredFormatter.prototype = {
604 __proto__: Formatter.prototype,
606 format: function (logMessage) {
608 _time: logMessage.time,
609 _namespace: logMessage.loggerName,
610 _level: logMessage.levelDesc
613 for (let key in logMessage.params) {
614 output[key] = logMessage.params[key];
617 if (!output.action) {
618 output.action = "UNKNOWN";
621 if (!output._message && logMessage.message) {
622 output._message = logMessage.message;
625 return JSON.stringify(output);
630 * Test an object to see if it is a Mozilla JS Error.
632 function isError(aObj) {
633 return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj &&
634 "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj);
638 * Parameter Formatters
639 * These massage an object used as a parameter for a LogMessage into
640 * a string representation of the object.
643 function ParameterFormatter() {
644 this._name = "ParameterFormatter"
646 ParameterFormatter.prototype = {
647 format: function(ob) {
649 if (ob === undefined) {
655 // Pass through primitive types and objects that unbox to primitive types.
656 if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") &&
657 typeof(ob) != "function") {
660 if (ob instanceof Ci.nsIException) {
661 return ob.toString() + " " + Log.stackTrace(ob);
663 else if (isError(ob)) {
664 return Log._formatError(ob);
666 // Just JSONify it. Filter out our internal fields and those the caller has
668 return JSON.stringify(ob, (key, val) => {
669 if (INTERNAL_FIELDS.has(key)) {
676 dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e));
678 // Fancy formatting failed. Just toSource() it - but even this may fail!
680 return ob.toSource();
692 * These can be attached to Loggers to log to different places
693 * Simply subclass and override doAppend to implement a new one
696 function Appender(formatter) {
697 this._name = "Appender";
698 this._formatter = formatter? formatter : new BasicFormatter();
700 Appender.prototype = {
701 level: Log.Level.All,
703 append: function App_append(message) {
705 this.doAppend(this._formatter.format(message));
708 toString: function App_toString() {
709 return this._name + " [level=" + this.level +
710 ", formatter=" + this._formatter + "]";
712 doAppend: function App_doAppend(formatted) {}
717 * Logs to standard out
720 function DumpAppender(formatter) {
721 Appender.call(this, formatter);
722 this._name = "DumpAppender";
724 DumpAppender.prototype = {
725 __proto__: Appender.prototype,
727 doAppend: function DApp_doAppend(formatted) {
728 dump(formatted + "\n");
734 * Logs to the javascript console
737 function ConsoleAppender(formatter) {
738 Appender.call(this, formatter);
739 this._name = "ConsoleAppender";
741 ConsoleAppender.prototype = {
742 __proto__: Appender.prototype,
744 // XXX this should be replaced with calls to the Browser Console
745 append: function App_append(message) {
747 let m = this._formatter.format(message);
748 if (message.level > Log.Level.Warn) {
756 doAppend: function CApp_doAppend(formatted) {
757 Cc["@mozilla.org/consoleservice;1"].
758 getService(Ci.nsIConsoleService).logStringMessage(formatted);
763 * Append to an nsIStorageStream
765 * This writes logging output to an in-memory stream which can later be read
766 * back as an nsIInputStream. It can be used to avoid expensive I/O operations
767 * during logging. Instead, one can periodically consume the input stream and
768 * e.g. write it to disk asynchronously.
770 function StorageStreamAppender(formatter) {
771 Appender.call(this, formatter);
772 this._name = "StorageStreamAppender";
775 StorageStreamAppender.prototype = {
776 __proto__: Appender.prototype,
778 _converterStream: null, // holds the nsIConverterOutputStream
779 _outputStream: null, // holds the underlying nsIOutputStream
784 if (!this._outputStream) {
785 // First create a raw stream. We can bail out early if that fails.
786 this._outputStream = this.newOutputStream();
787 if (!this._outputStream) {
791 // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
792 // the instance if we already have one.
793 if (!this._converterStream) {
794 this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
795 .createInstance(Ci.nsIConverterOutputStream);
797 this._converterStream.init(
798 this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
799 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
801 return this._converterStream;
804 newOutputStream: function newOutputStream() {
805 let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
806 .createInstance(Ci.nsIStorageStream);
807 ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
808 return ss.getOutputStream(0);
811 getInputStream: function getInputStream() {
815 return this._ss.newInputStream(0);
818 reset: function reset() {
819 if (!this._outputStream) {
822 this.outputStream.close();
823 this._outputStream = null;
827 doAppend: function (formatted) {
832 this.outputStream.writeString(formatted + "\n");
834 if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
835 // The underlying output stream is closed, so let's open a new one
837 this._outputStream = null;
839 this.outputStream.writeString(formatted + "\n");
841 // Ah well, we tried, but something seems to be hosed permanently.
850 * Writes output to file using OS.File.
852 function FileAppender(path, formatter) {
853 Appender.call(this, formatter);
854 this._name = "FileAppender";
855 this._encoder = new TextEncoder();
858 this._fileReadyPromise = null;
860 // This is a promise exposed for testing/debugging the logger itself.
861 this._lastWritePromise = null;
864 FileAppender.prototype = {
865 __proto__: Appender.prototype,
867 _openFile: function () {
868 return Task.spawn(function _openFile() {
870 this._file = yield OS.File.open(this._path,
873 if (err instanceof OS.File.Error) {
882 _getFile: function() {
883 if (!this._fileReadyPromise) {
884 this._fileReadyPromise = this._openFile();
887 return this._fileReadyPromise;
890 doAppend: function (formatted) {
891 let array = this._encoder.encode(formatted + "\n");
893 this._lastWritePromise = this._file.write(array);
895 this._lastWritePromise = this._getFile().then(_ => {
896 this._fileReadyPromise = null;
898 return this._file.write(array);
905 let fileClosePromise = this._file.close();
906 return fileClosePromise.then(_ => {
908 return OS.File.remove(this._path);
914 * Bounded File appender
916 * Writes output to file using OS.File. After the total message size
917 * (as defined by formatted.length) exceeds maxSize, existing messages
918 * will be discarded, and subsequent writes will be appended to a new log file.
920 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
921 FileAppender.call(this, path, formatter);
922 this._name = "BoundedFileAppender";
924 this._maxSize = maxSize;
925 this._closeFilePromise = null;
928 BoundedFileAppender.prototype = {
929 __proto__: FileAppender.prototype,
931 doAppend: function (formatted) {
932 if (!this._removeFilePromise) {
933 if (this._size < this._maxSize) {
934 this._size += formatted.length;
935 return FileAppender.prototype.doAppend.call(this, formatted);
937 this._removeFilePromise = this.reset();
939 this._removeFilePromise.then(_ => {
940 this._removeFilePromise = null;
941 this.doAppend(formatted);
946 let fileClosePromise;
947 if (this._fileReadyPromise) {
948 // An attempt to open the file may still be in progress.
949 fileClosePromise = this._fileReadyPromise.then(_ => {
950 return this._file.close();
953 fileClosePromise = this._file.close();
956 return fileClosePromise.then(_ => {
959 return OS.File.remove(this._path);