Bug 1608150 [wpt PR 21112] - Add missing space in `./wpt lint` command line docs...
[gecko.git] / toolkit / modules / Log.jsm
blob03111b6951c3c6f7f15d99ca178ecaf4081a7257
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 var EXPORTED_SYMBOLS = ["Log"];
9 const { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
12 ChromeUtils.defineModuleGetter(
13   this,
14   "Services",
15   "resource://gre/modules/Services.jsm"
17 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
20  * Dump a message everywhere we can if we have a failure.
21  */
22 function dumpError(text) {
23   dump(text + "\n");
24   Cu.reportError(text);
27 var Log = {
28   Level: {
29     Fatal: 70,
30     Error: 60,
31     Warn: 50,
32     Info: 40,
33     Config: 30,
34     Debug: 20,
35     Trace: 10,
36     All: -1, // We don't want All to be falsy.
37     Desc: {
38       70: "FATAL",
39       60: "ERROR",
40       50: "WARN",
41       40: "INFO",
42       30: "CONFIG",
43       20: "DEBUG",
44       10: "TRACE",
45       "-1": "ALL",
46     },
47     Numbers: {
48       FATAL: 70,
49       ERROR: 60,
50       WARN: 50,
51       INFO: 40,
52       CONFIG: 30,
53       DEBUG: 20,
54       TRACE: 10,
55       ALL: -1,
56     },
57   },
59   get repository() {
60     delete Log.repository;
61     Log.repository = new LoggerRepository();
62     return Log.repository;
63   },
64   set repository(value) {
65     delete Log.repository;
66     Log.repository = value;
67   },
69   _formatError(e) {
70     let result = String(e);
71     if (e.fileName) {
72       let loc = [e.fileName];
73       if (e.lineNumber) {
74         loc.push(e.lineNumber);
75       }
76       if (e.columnNumber) {
77         loc.push(e.columnNumber);
78       }
79       result += `(${loc.join(":")})`;
80     }
81     return `${result} ${Log.stackTrace(e)}`;
82   },
84   // This is for back compatibility with services/common/utils.js; we duplicate
85   // some of the logic in ParameterFormatter
86   exceptionStr(e) {
87     if (!e) {
88       return String(e);
89     }
90     if (e instanceof Ci.nsIException) {
91       return `${e} ${Log.stackTrace(e)}`;
92     } else if (isError(e)) {
93       return Log._formatError(e);
94     }
95     // else
96     let message = e.message || e;
97     return `${message} ${Log.stackTrace(e)}`;
98   },
100   stackTrace(e) {
101     // Wrapped nsIException
102     if (e.location) {
103       let frame = e.location;
104       let output = [];
105       while (frame) {
106         // Works on frames or exceptions, munges file:// URIs to shorten the paths
107         // FIXME: filename munging is sort of hackish, might be confusing if
108         // there are multiple extensions with similar filenames
109         let str = "<file:unknown>";
111         let file = frame.filename || frame.fileName;
112         if (file) {
113           str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
114         }
116         if (frame.lineNumber) {
117           str += ":" + frame.lineNumber;
118         }
120         if (frame.name) {
121           str = frame.name + "()@" + str;
122         }
124         if (str) {
125           output.push(str);
126         }
127         frame = frame.caller;
128       }
129       return `Stack trace: ${output.join("\n")}`;
130     }
131     // Standard JS exception
132     if (e.stack) {
133       let stack = e.stack;
134       return (
135         "JS Stack trace: " +
136         stack.trim().replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1")
137       );
138     }
140     return "No traceback available";
141   },
145  * LogMessage
146  * Encapsulates a single log event's data
147  */
148 class LogMessage {
149   constructor(loggerName, level, message, params) {
150     this.loggerName = loggerName;
151     this.level = level;
152     /*
153      * Special case to handle "log./level/(object)", for example logging a caught exception
154      * without providing text or params like: catch(e) { logger.warn(e) }
155      * Treating this as an empty text with the object in the 'params' field causes the
156      * object to be formatted properly by BasicFormatter.
157      */
158     if (
159       !params &&
160       message &&
161       typeof message == "object" &&
162       typeof message.valueOf() != "string"
163     ) {
164       this.message = null;
165       this.params = message;
166     } else {
167       // If the message text is empty, or a string, or a String object, normal handling
168       this.message = message;
169       this.params = params;
170     }
172     // The _structured field will correspond to whether this message is to
173     // be interpreted as a structured message.
174     this._structured = this.params && this.params.action;
175     this.time = Date.now();
176   }
178   get levelDesc() {
179     if (this.level in Log.Level.Desc) {
180       return Log.Level.Desc[this.level];
181     }
182     return "UNKNOWN";
183   }
185   toString() {
186     let msg = `${this.time} ${this.level} ${this.message}`;
187     if (this.params) {
188       msg += ` ${JSON.stringify(this.params)}`;
189     }
190     return `LogMessage [${msg}]`;
191   }
195  * Logger
196  * Hierarchical version.  Logs to all appenders, assigned or inherited
197  */
199 class Logger {
200   constructor(name, repository) {
201     if (!repository) {
202       repository = Log.repository;
203     }
204     this._name = name;
205     this.children = [];
206     this.ownAppenders = [];
207     this.appenders = [];
208     this._repository = repository;
210     this._levelPrefName = null;
211     this._levelPrefValue = null;
212     this._level = null;
213     this._parent = null;
214   }
216   get name() {
217     return this._name;
218   }
220   get level() {
221     if (this._levelPrefName) {
222       // We've been asked to use a preference to configure the logs. If the
223       // pref has a value we use it, otherwise we continue to use the parent.
224       const lpv = this._levelPrefValue;
225       if (lpv) {
226         const levelValue = Log.Level[lpv];
227         if (levelValue) {
228           // stash it in _level just in case a future value of the pref is
229           // invalid, in which case we end up continuing to use this value.
230           this._level = levelValue;
231           return levelValue;
232         }
233       } else {
234         // in case the pref has transitioned from a value to no value, we reset
235         // this._level and fall through to using the parent.
236         this._level = null;
237       }
238     }
239     if (this._level != null) {
240       return this._level;
241     }
242     if (this.parent) {
243       return this.parent.level;
244     }
245     dumpError("Log warning: root logger configuration error: no level defined");
246     return Log.Level.All;
247   }
248   set level(level) {
249     if (this._levelPrefName) {
250       // I guess we could honor this by nuking this._levelPrefValue, but it
251       // almost certainly implies confusion, so we'll warn and ignore.
252       dumpError(
253         `Log warning: The log '${this.name}' is configured to use ` +
254           `the preference '${this._levelPrefName}' - you must adjust ` +
255           `the level by setting this preference, not by using the ` +
256           `level setter`
257       );
258       return;
259     }
260     this._level = level;
261   }
263   get parent() {
264     return this._parent;
265   }
266   set parent(parent) {
267     if (this._parent == parent) {
268       return;
269     }
270     // Remove ourselves from parent's children
271     if (this._parent) {
272       let index = this._parent.children.indexOf(this);
273       if (index != -1) {
274         this._parent.children.splice(index, 1);
275       }
276     }
277     this._parent = parent;
278     parent.children.push(this);
279     this.updateAppenders();
280   }
282   manageLevelFromPref(prefName) {
283     if (prefName == this._levelPrefName) {
284       // We've already configured this log with an observer for that pref.
285       return;
286     }
287     if (this._levelPrefName) {
288       dumpError(
289         `The log '${this.name}' is already configured with the ` +
290           `preference '${this._levelPrefName}' - ignoring request to ` +
291           `also use the preference '${prefName}'`
292       );
293       return;
294     }
295     this._levelPrefName = prefName;
296     XPCOMUtils.defineLazyPreferenceGetter(this, "_levelPrefValue", prefName);
297   }
299   updateAppenders() {
300     if (this._parent) {
301       let notOwnAppenders = this._parent.appenders.filter(function(appender) {
302         return !this.ownAppenders.includes(appender);
303       }, this);
304       this.appenders = notOwnAppenders.concat(this.ownAppenders);
305     } else {
306       this.appenders = this.ownAppenders.slice();
307     }
309     // Update children's appenders.
310     for (let i = 0; i < this.children.length; i++) {
311       this.children[i].updateAppenders();
312     }
313   }
315   addAppender(appender) {
316     if (this.ownAppenders.includes(appender)) {
317       return;
318     }
319     this.ownAppenders.push(appender);
320     this.updateAppenders();
321   }
323   removeAppender(appender) {
324     let index = this.ownAppenders.indexOf(appender);
325     if (index == -1) {
326       return;
327     }
328     this.ownAppenders.splice(index, 1);
329     this.updateAppenders();
330   }
332   _unpackTemplateLiteral(string, params) {
333     if (!Array.isArray(params)) {
334       // Regular log() call.
335       return [string, params];
336     }
338     if (!Array.isArray(string)) {
339       // Not using template literal. However params was packed into an array by
340       // the this.[level] call, so we need to unpack it here.
341       return [string, params[0]];
342     }
344     // We're using template literal format (logger.warn `foo ${bar}`). Turn the
345     // template strings into one string containing "${0}"..."${n}" tokens, and
346     // feed it to the basic formatter. The formatter will treat the numbers as
347     // indices into the params array, and convert the tokens to the params.
349     if (!params.length) {
350       // No params; we need to set params to undefined, so the formatter
351       // doesn't try to output the params array.
352       return [string[0], undefined];
353     }
355     let concat = string[0];
356     for (let i = 0; i < params.length; i++) {
357       concat += `\${${i}}${string[i + 1]}`;
358     }
359     return [concat, params];
360   }
362   log(level, string, params) {
363     if (this.level > level) {
364       return;
365     }
367     // Hold off on creating the message object until we actually have
368     // an appender that's responsible.
369     let message;
370     let appenders = this.appenders;
371     for (let appender of appenders) {
372       if (appender.level > level) {
373         continue;
374       }
375       if (!message) {
376         [string, params] = this._unpackTemplateLiteral(string, params);
377         message = new LogMessage(this._name, level, string, params);
378       }
379       appender.append(message);
380     }
381   }
383   fatal(string, ...params) {
384     this.log(Log.Level.Fatal, string, params);
385   }
386   error(string, ...params) {
387     this.log(Log.Level.Error, string, params);
388   }
389   warn(string, ...params) {
390     this.log(Log.Level.Warn, string, params);
391   }
392   info(string, ...params) {
393     this.log(Log.Level.Info, string, params);
394   }
395   config(string, ...params) {
396     this.log(Log.Level.Config, string, params);
397   }
398   debug(string, ...params) {
399     this.log(Log.Level.Debug, string, params);
400   }
401   trace(string, ...params) {
402     this.log(Log.Level.Trace, string, params);
403   }
407  * LoggerRepository
408  * Implements a hierarchy of Loggers
409  */
411 class LoggerRepository {
412   constructor() {
413     this._loggers = {};
414     this._rootLogger = null;
415   }
417   get rootLogger() {
418     if (!this._rootLogger) {
419       this._rootLogger = new Logger("root", this);
420       this._rootLogger.level = Log.Level.All;
421     }
422     return this._rootLogger;
423   }
424   set rootLogger(logger) {
425     throw new Error("Cannot change the root logger");
426   }
428   _updateParents(name) {
429     let pieces = name.split(".");
430     let cur, parent;
432     // find the closest parent
433     // don't test for the logger name itself, as there's a chance it's already
434     // there in this._loggers
435     for (let i = 0; i < pieces.length - 1; i++) {
436       if (cur) {
437         cur += "." + pieces[i];
438       } else {
439         cur = pieces[i];
440       }
441       if (cur in this._loggers) {
442         parent = cur;
443       }
444     }
446     // if we didn't assign a parent above, there is no parent
447     if (!parent) {
448       this._loggers[name].parent = this.rootLogger;
449     } else {
450       this._loggers[name].parent = this._loggers[parent];
451     }
453     // trigger updates for any possible descendants of this logger
454     for (let logger in this._loggers) {
455       if (logger != name && logger.indexOf(name) == 0) {
456         this._updateParents(logger);
457       }
458     }
459   }
461   /**
462    * Obtain a named Logger.
463    *
464    * The returned Logger instance for a particular name is shared among
465    * all callers. In other words, if two consumers call getLogger("foo"),
466    * they will both have a reference to the same object.
467    *
468    * @return Logger
469    */
470   getLogger(name) {
471     if (name in this._loggers) {
472       return this._loggers[name];
473     }
474     this._loggers[name] = new Logger(name, this);
475     this._updateParents(name);
476     return this._loggers[name];
477   }
479   /**
480    * Obtain a Logger that logs all string messages with a prefix.
481    *
482    * A common pattern is to have separate Logger instances for each instance
483    * of an object. But, you still want to distinguish between each instance.
484    * Since Log.repository.getLogger() returns shared Logger objects,
485    * monkeypatching one Logger modifies them all.
486    *
487    * This function returns a new object with a prototype chain that chains
488    * up to the original Logger instance. The new prototype has log functions
489    * that prefix content to each message.
490    *
491    * @param name
492    *        (string) The Logger to retrieve.
493    * @param prefix
494    *        (string) The string to prefix each logged message with.
495    */
496   getLoggerWithMessagePrefix(name, prefix) {
497     let log = this.getLogger(name);
499     let proxy = Object.create(log);
500     proxy.log = (level, string, params) => {
501       if (Array.isArray(string) && Array.isArray(params)) {
502         // Template literal.
503         // We cannot change the original array, so create a new one.
504         string = [prefix + string[0]].concat(string.slice(1));
505       } else {
506         string = prefix + string; // Regular string.
507       }
508       return log.log(level, string, params);
509     };
510     return proxy;
511   }
515  * Formatters
516  * These massage a LogMessage into whatever output is desired.
517  */
519 // Basic formatter that doesn't do anything fancy.
520 class BasicFormatter {
521   constructor(dateFormat) {
522     if (dateFormat) {
523       this.dateFormat = dateFormat;
524     }
525     this.parameterFormatter = new ParameterFormatter();
526   }
528   /**
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
534    * to the message.
535    */
536   formatText(message) {
537     let params = message.params;
538     if (typeof params == "undefined") {
539       return message.message || "";
540     }
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 (this.parameterFormatter) {
547       // have we successfully substituted any parameters into the message?
548       // in the log message
549       let subDone = false;
550       let regex = /\$\{(\S*?)\}/g;
551       let textParts = [];
552       if (message.message) {
553         textParts.push(
554           message.message.replace(regex, (_, sub) => {
555             // ${foo} means use the params['foo']
556             if (sub) {
557               if (pIsObject && sub in message.params) {
558                 subDone = true;
559                 return this.parameterFormatter.format(message.params[sub]);
560               }
561               return "${" + sub + "}";
562             }
563             // ${} means use the entire params object.
564             subDone = true;
565             return this.parameterFormatter.format(message.params);
566           })
567         );
568       }
569       if (!subDone) {
570         // There were no substitutions in the text, so format the entire params object
571         let rest = this.parameterFormatter.format(message.params);
572         if (rest !== null && rest != "{}") {
573           textParts.push(rest);
574         }
575       }
576       return textParts.join(": ");
577     }
578     return undefined;
579   }
581   format(message) {
582     return (
583       message.time +
584       "\t" +
585       message.loggerName +
586       "\t" +
587       message.levelDesc +
588       "\t" +
589       this.formatText(message)
590     );
591   }
595  * Test an object to see if it is a Mozilla JS Error.
596  */
597 function isError(aObj) {
598   return (
599     aObj &&
600     typeof aObj == "object" &&
601     "name" in aObj &&
602     "message" in aObj &&
603     "fileName" in aObj &&
604     "lineNumber" in aObj &&
605     "stack" in aObj
606   );
610  * Parameter Formatters
611  * These massage an object used as a parameter for a LogMessage into
612  * a string representation of the object.
613  */
615 class ParameterFormatter {
616   constructor() {
617     this._name = "ParameterFormatter";
618   }
620   format(ob) {
621     try {
622       if (ob === undefined) {
623         return "undefined";
624       }
625       if (ob === null) {
626         return "null";
627       }
628       // Pass through primitive types and objects that unbox to primitive types.
629       if (
630         (typeof ob != "object" || typeof ob.valueOf() != "object") &&
631         typeof ob != "function"
632       ) {
633         return ob;
634       }
635       if (ob instanceof Ci.nsIException) {
636         return `${ob} ${Log.stackTrace(ob)}`;
637       } else if (isError(ob)) {
638         return Log._formatError(ob);
639       }
640       // Just JSONify it. Filter out our internal fields and those the caller has
641       // already handled.
642       return JSON.stringify(ob, (key, val) => {
643         if (INTERNAL_FIELDS.has(key)) {
644           return undefined;
645         }
646         return val;
647       });
648     } catch (e) {
649       dumpError(
650         `Exception trying to format object for log message: ${Log.exceptionStr(
651           e
652         )}`
653       );
654     }
655     // Fancy formatting failed. Just toSource() it - but even this may fail!
656     try {
657       return ob.toSource();
658     } catch (_) {}
659     try {
660       return String(ob);
661     } catch (_) {
662       return "[object]";
663     }
664   }
668  * Appenders
669  * These can be attached to Loggers to log to different places
670  * Simply subclass and override doAppend to implement a new one
671  */
673 class Appender {
674   constructor(formatter) {
675     this.level = Log.Level.All;
676     this._name = "Appender";
677     this._formatter = formatter || new BasicFormatter();
678   }
680   append(message) {
681     if (message) {
682       this.doAppend(this._formatter.format(message));
683     }
684   }
686   toString() {
687     return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
688   }
692  * DumpAppender
693  * Logs to standard out
694  */
696 class DumpAppender extends Appender {
697   constructor(formatter) {
698     super(formatter);
699     this._name = "DumpAppender";
700   }
702   doAppend(formatted) {
703     dump(formatted + "\n");
704   }
708  * ConsoleAppender
709  * Logs to the javascript console
710  */
712 class ConsoleAppender extends Appender {
713   constructor(formatter) {
714     super(formatter);
715     this._name = "ConsoleAppender";
716   }
718   // XXX this should be replaced with calls to the Browser Console
719   append(message) {
720     if (message) {
721       let m = this._formatter.format(message);
722       if (message.level > Log.Level.Warn) {
723         Cu.reportError(m);
724         return;
725       }
726       this.doAppend(m);
727     }
728   }
730   doAppend(formatted) {
731     Services.console.logStringMessage(formatted);
732   }
735 Object.assign(Log, {
736   LogMessage,
737   Logger,
738   LoggerRepository,
740   BasicFormatter,
742   Appender,
743   DumpAppender,
744   ConsoleAppender,
746   ParameterFormatter,