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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
10 * Dump a message everywhere we can if we have a failure.
12 function dumpError(text) {
14 // TODO: Bug 1801091 - Figure out how to replace this.
15 // eslint-disable-next-line mozilla/no-cu-reportError
28 All: -1, // We don't want All to be falsy.
52 delete Log.repository;
53 Log.repository = new LoggerRepository();
54 return Log.repository;
56 set repository(value) {
57 delete Log.repository;
58 Log.repository = value;
62 let result = String(e);
64 let loc = [e.fileName];
66 loc.push(e.lineNumber);
69 loc.push(e.columnNumber);
71 result += `(${loc.join(":")})`;
73 return `${result} ${Log.stackTrace(e)}`;
76 // This is for back compatibility with services/common/utils.js; we duplicate
77 // some of the logic in ParameterFormatter
82 if (e instanceof Ci.nsIException) {
83 return `${e} ${Log.stackTrace(e)}`;
84 } else if (isError(e)) {
85 return Log._formatError(e);
88 let message = e.message || e;
89 return `${message} ${Log.stackTrace(e)}`;
94 return Components.stack.caller.formattedStack.trim();
96 // Wrapped nsIException
98 let frame = e.location;
101 // Works on frames or exceptions, munges file:// URIs to shorten the paths
102 // FIXME: filename munging is sort of hackish.
103 let str = "<file:unknown>";
105 let file = frame.filename || frame.fileName;
107 str = file.replace(/^(?:chrome|file):.*?([^\/\.]+(\.\w+)+)$/, "$1");
110 if (frame.lineNumber) {
111 str += ":" + frame.lineNumber;
115 str = frame.name + "()@" + str;
121 frame = frame.caller;
123 return `Stack trace: ${output.join("\n")}`;
125 // Standard JS exception
130 stack.trim().replace(/@[^@]*?([^\/\.]+(\.\w+)+:)/g, "@$1")
134 if (e instanceof Ci.nsIStackFrame) {
135 return e.formattedStack.trim();
137 return "No traceback available";
143 * Encapsulates a single log event's data
146 constructor(loggerName, level, message, params) {
147 this.loggerName = loggerName;
150 * Special case to handle "log./level/(object)", for example logging a caught exception
151 * without providing text or params like: catch(e) { logger.warn(e) }
152 * Treating this as an empty text with the object in the 'params' field causes the
153 * object to be formatted properly by BasicFormatter.
158 typeof message == "object" &&
159 typeof message.valueOf() != "string"
162 this.params = message;
164 // If the message text is empty, or a string, or a String object, normal handling
165 this.message = message;
166 this.params = params;
169 // The _structured field will correspond to whether this message is to
170 // be interpreted as a structured message.
171 this._structured = this.params && this.params.action;
172 this.time = Date.now();
176 if (this.level in Log.Level.Desc) {
177 return Log.Level.Desc[this.level];
183 let msg = `${this.time} ${this.level} ${this.message}`;
185 msg += ` ${JSON.stringify(this.params)}`;
187 return `LogMessage [${msg}]`;
193 * Hierarchical version. Logs to all appenders, assigned or inherited
197 constructor(name, repository) {
199 repository = Log.repository;
203 this.ownAppenders = [];
205 this._repository = repository;
207 this._levelPrefName = null;
208 this._levelPrefValue = null;
218 if (this._levelPrefName) {
219 // We've been asked to use a preference to configure the logs. If the
220 // pref has a value we use it, otherwise we continue to use the parent.
221 const lpv = this._levelPrefValue;
223 const levelValue = Log.Level[lpv];
225 // stash it in _level just in case a future value of the pref is
226 // invalid, in which case we end up continuing to use this value.
227 this._level = levelValue;
231 // in case the pref has transitioned from a value to no value, we reset
232 // this._level and fall through to using the parent.
236 if (this._level != null) {
240 return this.parent.level;
242 dumpError("Log warning: root logger configuration error: no level defined");
243 return Log.Level.All;
246 if (this._levelPrefName) {
247 // I guess we could honor this by nuking this._levelPrefValue, but it
248 // almost certainly implies confusion, so we'll warn and ignore.
250 `Log warning: The log '${this.name}' is configured to use ` +
251 `the preference '${this._levelPrefName}' - you must adjust ` +
252 `the level by setting this preference, not by using the ` +
264 if (this._parent == parent) {
267 // Remove ourselves from parent's children
269 let index = this._parent.children.indexOf(this);
271 this._parent.children.splice(index, 1);
274 this._parent = parent;
275 parent.children.push(this);
276 this.updateAppenders();
279 manageLevelFromPref(prefName) {
280 if (prefName == this._levelPrefName) {
281 // We've already configured this log with an observer for that pref.
284 if (this._levelPrefName) {
286 `The log '${this.name}' is already configured with the ` +
287 `preference '${this._levelPrefName}' - ignoring request to ` +
288 `also use the preference '${prefName}'`
292 this._levelPrefName = prefName;
293 XPCOMUtils.defineLazyPreferenceGetter(this, "_levelPrefValue", prefName);
298 let notOwnAppenders = this._parent.appenders.filter(function (appender) {
299 return !this.ownAppenders.includes(appender);
301 this.appenders = notOwnAppenders.concat(this.ownAppenders);
303 this.appenders = this.ownAppenders.slice();
306 // Update children's appenders.
307 for (let i = 0; i < this.children.length; i++) {
308 this.children[i].updateAppenders();
312 addAppender(appender) {
313 if (this.ownAppenders.includes(appender)) {
316 this.ownAppenders.push(appender);
317 this.updateAppenders();
320 removeAppender(appender) {
321 let index = this.ownAppenders.indexOf(appender);
325 this.ownAppenders.splice(index, 1);
326 this.updateAppenders();
329 _unpackTemplateLiteral(string, params) {
330 if (!Array.isArray(params)) {
331 // Regular log() call.
332 return [string, params];
335 if (!Array.isArray(string)) {
336 // Not using template literal. However params was packed into an array by
337 // the this.[level] call, so we need to unpack it here.
338 return [string, params[0]];
341 // We're using template literal format (logger.warn `foo ${bar}`). Turn the
342 // template strings into one string containing "${0}"..."${n}" tokens, and
343 // feed it to the basic formatter. The formatter will treat the numbers as
344 // indices into the params array, and convert the tokens to the params.
346 if (!params.length) {
347 // No params; we need to set params to undefined, so the formatter
348 // doesn't try to output the params array.
349 return [string[0], undefined];
352 let concat = string[0];
353 for (let i = 0; i < params.length; i++) {
354 concat += `\${${i}}${string[i + 1]}`;
356 return [concat, params];
359 log(level, string, params) {
360 if (this.level > level) {
364 // Hold off on creating the message object until we actually have
365 // an appender that's responsible.
367 let appenders = this.appenders;
368 for (let appender of appenders) {
369 if (appender.level > level) {
373 [string, params] = this._unpackTemplateLiteral(string, params);
374 message = new LogMessage(this._name, level, string, params);
376 appender.append(message);
380 fatal(string, ...params) {
381 this.log(Log.Level.Fatal, string, params);
383 error(string, ...params) {
384 this.log(Log.Level.Error, string, params);
386 warn(string, ...params) {
387 this.log(Log.Level.Warn, string, params);
389 info(string, ...params) {
390 this.log(Log.Level.Info, string, params);
392 config(string, ...params) {
393 this.log(Log.Level.Config, string, params);
395 debug(string, ...params) {
396 this.log(Log.Level.Debug, string, params);
398 trace(string, ...params) {
399 this.log(Log.Level.Trace, string, params);
405 * Implements a hierarchy of Loggers
408 class LoggerRepository {
411 this._rootLogger = null;
415 if (!this._rootLogger) {
416 this._rootLogger = new Logger("root", this);
417 this._rootLogger.level = Log.Level.All;
419 return this._rootLogger;
421 set rootLogger(logger) {
422 throw new Error("Cannot change the root logger");
425 _updateParents(name) {
426 let pieces = name.split(".");
429 // find the closest parent
430 // don't test for the logger name itself, as there's a chance it's already
431 // there in this._loggers
432 for (let i = 0; i < pieces.length - 1; i++) {
434 cur += "." + pieces[i];
438 if (cur in this._loggers) {
443 // if we didn't assign a parent above, there is no parent
445 this._loggers[name].parent = this.rootLogger;
447 this._loggers[name].parent = this._loggers[parent];
450 // trigger updates for any possible descendants of this logger
451 for (let logger in this._loggers) {
452 if (logger != name && logger.indexOf(name) == 0) {
453 this._updateParents(logger);
459 * Obtain a named Logger.
461 * The returned Logger instance for a particular name is shared among
462 * all callers. In other words, if two consumers call getLogger("foo"),
463 * they will both have a reference to the same object.
468 if (name in this._loggers) {
469 return this._loggers[name];
471 this._loggers[name] = new Logger(name, this);
472 this._updateParents(name);
473 return this._loggers[name];
477 * Obtain a Logger that logs all string messages with a prefix.
479 * A common pattern is to have separate Logger instances for each instance
480 * of an object. But, you still want to distinguish between each instance.
481 * Since Log.repository.getLogger() returns shared Logger objects,
482 * monkeypatching one Logger modifies them all.
484 * This function returns a new object with a prototype chain that chains
485 * up to the original Logger instance. The new prototype has log functions
486 * that prefix content to each message.
489 * (string) The Logger to retrieve.
491 * (string) The string to prefix each logged message with.
493 getLoggerWithMessagePrefix(name, prefix) {
494 let log = this.getLogger(name);
496 let proxy = Object.create(log);
497 proxy.log = (level, string, params) => {
498 if (Array.isArray(string) && Array.isArray(params)) {
500 // We cannot change the original array, so create a new one.
501 string = [prefix + string[0]].concat(string.slice(1));
503 string = prefix + string; // Regular string.
505 return log.log(level, string, params);
513 * These massage a LogMessage into whatever output is desired.
516 // Basic formatter that doesn't do anything fancy.
517 class BasicFormatter {
518 constructor(dateFormat) {
520 this.dateFormat = dateFormat;
522 this.parameterFormatter = new ParameterFormatter();
526 * Format the text of a message with optional parameters.
527 * If the text contains ${identifier}, replace that with
528 * the value of params[identifier]; if ${}, replace that with
529 * the entire params object. If no params have been substituted
530 * into the text, format the entire object and append that
533 formatText(message) {
534 let params = message.params;
535 if (typeof params == "undefined") {
536 return message.message || "";
538 // Defensive handling of non-object params
539 // We could add a special case for NSRESULT values here...
540 let pIsObject = typeof params == "object" || typeof params == "function";
542 // if we have params, try and find substitutions.
543 if (this.parameterFormatter) {
544 // have we successfully substituted any parameters into the message?
545 // in the log message
547 let regex = /\$\{(\S*?)\}/g;
549 if (message.message) {
551 message.message.replace(regex, (_, sub) => {
552 // ${foo} means use the params['foo']
554 if (pIsObject && sub in message.params) {
556 return this.parameterFormatter.format(message.params[sub]);
558 return "${" + sub + "}";
560 // ${} means use the entire params object.
562 return this.parameterFormatter.format(message.params);
567 // There were no substitutions in the text, so format the entire params object
568 let rest = this.parameterFormatter.format(message.params);
569 if (rest !== null && rest != "{}") {
570 textParts.push(rest);
573 return textParts.join(": ");
586 this.formatText(message)
592 * Test an object to see if it is a Mozilla JS Error.
594 function isError(aObj) {
597 typeof aObj == "object" &&
600 "fileName" in aObj &&
601 "lineNumber" in aObj &&
607 * Parameter Formatters
608 * These massage an object used as a parameter for a LogMessage into
609 * a string representation of the object.
612 class ParameterFormatter {
614 this._name = "ParameterFormatter";
619 if (ob === undefined) {
625 // Pass through primitive types and objects that unbox to primitive types.
627 (typeof ob != "object" || typeof ob.valueOf() != "object") &&
628 typeof ob != "function"
632 if (ob instanceof Ci.nsIException) {
633 return `${ob} ${Log.stackTrace(ob)}`;
634 } else if (isError(ob)) {
635 return Log._formatError(ob);
637 // Just JSONify it. Filter out our internal fields and those the caller has
639 return JSON.stringify(ob, (key, val) => {
640 if (INTERNAL_FIELDS.has(key)) {
647 `Exception trying to format object for log message: ${Log.exceptionStr(
652 // Fancy formatting failed. Just toSource() it - but even this may fail!
654 return ob.toSource();
666 * These can be attached to Loggers to log to different places
667 * Simply subclass and override doAppend to implement a new one
671 constructor(formatter) {
672 this.level = Log.Level.All;
673 this._name = "Appender";
674 this._formatter = formatter || new BasicFormatter();
679 this.doAppend(this._formatter.format(message));
684 return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
690 * Logs to standard out
693 class DumpAppender extends Appender {
694 constructor(formatter) {
696 this._name = "DumpAppender";
699 doAppend(formatted) {
700 dump(formatted + "\n");
706 * Logs to the javascript console
709 class ConsoleAppender extends Appender {
710 constructor(formatter) {
712 this._name = "ConsoleAppender";
715 // XXX this should be replaced with calls to the Browser Console
718 let m = this._formatter.format(message);
719 if (message.level > Log.Level.Warn) {
720 // TODO: Bug 1801091 - Figure out how to replace this.
721 // eslint-disable-next-line mozilla/no-cu-reportError
729 doAppend(formatted) {
730 Services.console.logStringMessage(formatted);