summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/Log.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/Log.sys.mjs')
-rw-r--r--toolkit/modules/Log.sys.mjs746
1 files changed, 746 insertions, 0 deletions
diff --git a/toolkit/modules/Log.sys.mjs b/toolkit/modules/Log.sys.mjs
new file mode 100644
index 0000000000..62cd80b15c
--- /dev/null
+++ b/toolkit/modules/Log.sys.mjs
@@ -0,0 +1,746 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
+
+/*
+ * Dump a message everywhere we can if we have a failure.
+ */
+function dumpError(text) {
+ dump(text + "\n");
+ // TODO: Bug 1801091 - Figure out how to replace this.
+ // eslint-disable-next-line mozilla/no-cu-reportError
+ Cu.reportError(text);
+}
+
+export var Log = {
+ Level: {
+ Fatal: 70,
+ Error: 60,
+ Warn: 50,
+ Info: 40,
+ Config: 30,
+ Debug: 20,
+ Trace: 10,
+ All: -1, // We don't want All to be falsy.
+ Desc: {
+ 70: "FATAL",
+ 60: "ERROR",
+ 50: "WARN",
+ 40: "INFO",
+ 30: "CONFIG",
+ 20: "DEBUG",
+ 10: "TRACE",
+ "-1": "ALL",
+ },
+ Numbers: {
+ FATAL: 70,
+ ERROR: 60,
+ WARN: 50,
+ INFO: 40,
+ CONFIG: 30,
+ DEBUG: 20,
+ TRACE: 10,
+ ALL: -1,
+ },
+ },
+
+ get repository() {
+ delete Log.repository;
+ Log.repository = new LoggerRepository();
+ return Log.repository;
+ },
+ set repository(value) {
+ delete Log.repository;
+ Log.repository = value;
+ },
+
+ _formatError(e) {
+ let result = String(e);
+ if (e.fileName) {
+ let loc = [e.fileName];
+ if (e.lineNumber) {
+ loc.push(e.lineNumber);
+ }
+ if (e.columnNumber) {
+ loc.push(e.columnNumber);
+ }
+ result += `(${loc.join(":")})`;
+ }
+ return `${result} ${Log.stackTrace(e)}`;
+ },
+
+ // This is for back compatibility with services/common/utils.js; we duplicate
+ // some of the logic in ParameterFormatter
+ exceptionStr(e) {
+ if (!e) {
+ return String(e);
+ }
+ if (e instanceof Ci.nsIException) {
+ return `${e} ${Log.stackTrace(e)}`;
+ } else if (isError(e)) {
+ return Log._formatError(e);
+ }
+ // else
+ let message = e.message || e;
+ return `${message} ${Log.stackTrace(e)}`;
+ },
+
+ stackTrace(e) {
+ if (!e) {
+ return Components.stack.caller.formattedStack.trim();
+ }
+ // Wrapped nsIException
+ if (e.location) {
+ let frame = e.location;
+ let output = [];
+ while (frame) {
+ // Works on frames or exceptions, munges file:// URIs to shorten the paths
+ // FIXME: filename munging is sort of hackish.
+ let str = "<file:unknown>";
+
+ let file = frame.filename || frame.fileName;
+ if (file) {
+ str = file.replace(/^(?:chrome|file):.*?([^\/\.]+(\.\w+)+)$/, "$1");
+ }
+
+ if (frame.lineNumber) {
+ str += ":" + frame.lineNumber;
+ }
+
+ if (frame.name) {
+ str = frame.name + "()@" + str;
+ }
+
+ if (str) {
+ output.push(str);
+ }
+ frame = frame.caller;
+ }
+ return `Stack trace: ${output.join("\n")}`;
+ }
+ // Standard JS exception
+ if (e.stack) {
+ let stack = e.stack;
+ return (
+ "JS Stack trace: " +
+ stack.trim().replace(/@[^@]*?([^\/\.]+(\.\w+)+:)/g, "@$1")
+ );
+ }
+
+ if (e instanceof Ci.nsIStackFrame) {
+ return e.formattedStack.trim();
+ }
+ return "No traceback available";
+ },
+};
+
+/*
+ * LogMessage
+ * Encapsulates a single log event's data
+ */
+class LogMessage {
+ constructor(loggerName, level, message, params) {
+ this.loggerName = loggerName;
+ this.level = level;
+ /*
+ * Special case to handle "log./level/(object)", for example logging a caught exception
+ * without providing text or params like: catch(e) { logger.warn(e) }
+ * Treating this as an empty text with the object in the 'params' field causes the
+ * object to be formatted properly by BasicFormatter.
+ */
+ if (
+ !params &&
+ message &&
+ typeof message == "object" &&
+ typeof message.valueOf() != "string"
+ ) {
+ this.message = null;
+ this.params = message;
+ } else {
+ // If the message text is empty, or a string, or a String object, normal handling
+ this.message = message;
+ this.params = params;
+ }
+
+ // The _structured field will correspond to whether this message is to
+ // be interpreted as a structured message.
+ this._structured = this.params && this.params.action;
+ this.time = Date.now();
+ }
+
+ get levelDesc() {
+ if (this.level in Log.Level.Desc) {
+ return Log.Level.Desc[this.level];
+ }
+ return "UNKNOWN";
+ }
+
+ toString() {
+ let msg = `${this.time} ${this.level} ${this.message}`;
+ if (this.params) {
+ msg += ` ${JSON.stringify(this.params)}`;
+ }
+ return `LogMessage [${msg}]`;
+ }
+}
+
+/*
+ * Logger
+ * Hierarchical version. Logs to all appenders, assigned or inherited
+ */
+
+class Logger {
+ constructor(name, repository) {
+ if (!repository) {
+ repository = Log.repository;
+ }
+ this._name = name;
+ this.children = [];
+ this.ownAppenders = [];
+ this.appenders = [];
+ this._repository = repository;
+
+ this._levelPrefName = null;
+ this._levelPrefValue = null;
+ this._level = null;
+ this._parent = null;
+ }
+
+ get name() {
+ return this._name;
+ }
+
+ get level() {
+ if (this._levelPrefName) {
+ // We've been asked to use a preference to configure the logs. If the
+ // pref has a value we use it, otherwise we continue to use the parent.
+ const lpv = this._levelPrefValue;
+ if (lpv) {
+ const levelValue = Log.Level[lpv];
+ if (levelValue) {
+ // stash it in _level just in case a future value of the pref is
+ // invalid, in which case we end up continuing to use this value.
+ this._level = levelValue;
+ return levelValue;
+ }
+ } else {
+ // in case the pref has transitioned from a value to no value, we reset
+ // this._level and fall through to using the parent.
+ this._level = null;
+ }
+ }
+ if (this._level != null) {
+ return this._level;
+ }
+ if (this.parent) {
+ return this.parent.level;
+ }
+ dumpError("Log warning: root logger configuration error: no level defined");
+ return Log.Level.All;
+ }
+ set level(level) {
+ if (this._levelPrefName) {
+ // I guess we could honor this by nuking this._levelPrefValue, but it
+ // almost certainly implies confusion, so we'll warn and ignore.
+ dumpError(
+ `Log warning: The log '${this.name}' is configured to use ` +
+ `the preference '${this._levelPrefName}' - you must adjust ` +
+ `the level by setting this preference, not by using the ` +
+ `level setter`
+ );
+ return;
+ }
+ this._level = level;
+ }
+
+ get parent() {
+ return this._parent;
+ }
+ set parent(parent) {
+ if (this._parent == parent) {
+ return;
+ }
+ // Remove ourselves from parent's children
+ if (this._parent) {
+ let index = this._parent.children.indexOf(this);
+ if (index != -1) {
+ this._parent.children.splice(index, 1);
+ }
+ }
+ this._parent = parent;
+ parent.children.push(this);
+ this.updateAppenders();
+ }
+
+ manageLevelFromPref(prefName) {
+ if (prefName == this._levelPrefName) {
+ // We've already configured this log with an observer for that pref.
+ return;
+ }
+ if (this._levelPrefName) {
+ dumpError(
+ `The log '${this.name}' is already configured with the ` +
+ `preference '${this._levelPrefName}' - ignoring request to ` +
+ `also use the preference '${prefName}'`
+ );
+ return;
+ }
+ this._levelPrefName = prefName;
+ XPCOMUtils.defineLazyPreferenceGetter(this, "_levelPrefValue", prefName);
+ }
+
+ updateAppenders() {
+ if (this._parent) {
+ let notOwnAppenders = this._parent.appenders.filter(function (appender) {
+ return !this.ownAppenders.includes(appender);
+ }, this);
+ this.appenders = notOwnAppenders.concat(this.ownAppenders);
+ } else {
+ this.appenders = this.ownAppenders.slice();
+ }
+
+ // Update children's appenders.
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].updateAppenders();
+ }
+ }
+
+ addAppender(appender) {
+ if (this.ownAppenders.includes(appender)) {
+ return;
+ }
+ this.ownAppenders.push(appender);
+ this.updateAppenders();
+ }
+
+ removeAppender(appender) {
+ let index = this.ownAppenders.indexOf(appender);
+ if (index == -1) {
+ return;
+ }
+ this.ownAppenders.splice(index, 1);
+ this.updateAppenders();
+ }
+
+ _unpackTemplateLiteral(string, params) {
+ if (!Array.isArray(params)) {
+ // Regular log() call.
+ return [string, params];
+ }
+
+ if (!Array.isArray(string)) {
+ // Not using template literal. However params was packed into an array by
+ // the this.[level] call, so we need to unpack it here.
+ return [string, params[0]];
+ }
+
+ // We're using template literal format (logger.warn `foo ${bar}`). Turn the
+ // template strings into one string containing "${0}"..."${n}" tokens, and
+ // feed it to the basic formatter. The formatter will treat the numbers as
+ // indices into the params array, and convert the tokens to the params.
+
+ if (!params.length) {
+ // No params; we need to set params to undefined, so the formatter
+ // doesn't try to output the params array.
+ return [string[0], undefined];
+ }
+
+ let concat = string[0];
+ for (let i = 0; i < params.length; i++) {
+ concat += `\${${i}}${string[i + 1]}`;
+ }
+ return [concat, params];
+ }
+
+ log(level, string, params) {
+ if (this.level > level) {
+ return;
+ }
+
+ // Hold off on creating the message object until we actually have
+ // an appender that's responsible.
+ let message;
+ let appenders = this.appenders;
+ for (let appender of appenders) {
+ if (appender.level > level) {
+ continue;
+ }
+ if (!message) {
+ [string, params] = this._unpackTemplateLiteral(string, params);
+ message = new LogMessage(this._name, level, string, params);
+ }
+ appender.append(message);
+ }
+ }
+
+ fatal(string, ...params) {
+ this.log(Log.Level.Fatal, string, params);
+ }
+ error(string, ...params) {
+ this.log(Log.Level.Error, string, params);
+ }
+ warn(string, ...params) {
+ this.log(Log.Level.Warn, string, params);
+ }
+ info(string, ...params) {
+ this.log(Log.Level.Info, string, params);
+ }
+ config(string, ...params) {
+ this.log(Log.Level.Config, string, params);
+ }
+ debug(string, ...params) {
+ this.log(Log.Level.Debug, string, params);
+ }
+ trace(string, ...params) {
+ this.log(Log.Level.Trace, string, params);
+ }
+}
+
+/*
+ * LoggerRepository
+ * Implements a hierarchy of Loggers
+ */
+
+class LoggerRepository {
+ constructor() {
+ this._loggers = {};
+ this._rootLogger = null;
+ }
+
+ get rootLogger() {
+ if (!this._rootLogger) {
+ this._rootLogger = new Logger("root", this);
+ this._rootLogger.level = Log.Level.All;
+ }
+ return this._rootLogger;
+ }
+ set rootLogger(logger) {
+ throw new Error("Cannot change the root logger");
+ }
+
+ _updateParents(name) {
+ let pieces = name.split(".");
+ let cur, parent;
+
+ // find the closest parent
+ // don't test for the logger name itself, as there's a chance it's already
+ // there in this._loggers
+ for (let i = 0; i < pieces.length - 1; i++) {
+ if (cur) {
+ cur += "." + pieces[i];
+ } else {
+ cur = pieces[i];
+ }
+ if (cur in this._loggers) {
+ parent = cur;
+ }
+ }
+
+ // if we didn't assign a parent above, there is no parent
+ if (!parent) {
+ this._loggers[name].parent = this.rootLogger;
+ } else {
+ this._loggers[name].parent = this._loggers[parent];
+ }
+
+ // trigger updates for any possible descendants of this logger
+ for (let logger in this._loggers) {
+ if (logger != name && logger.indexOf(name) == 0) {
+ this._updateParents(logger);
+ }
+ }
+ }
+
+ /**
+ * Obtain a named Logger.
+ *
+ * The returned Logger instance for a particular name is shared among
+ * all callers. In other words, if two consumers call getLogger("foo"),
+ * they will both have a reference to the same object.
+ *
+ * @return Logger
+ */
+ getLogger(name) {
+ if (name in this._loggers) {
+ return this._loggers[name];
+ }
+ this._loggers[name] = new Logger(name, this);
+ this._updateParents(name);
+ return this._loggers[name];
+ }
+
+ /**
+ * Obtain a Logger that logs all string messages with a prefix.
+ *
+ * A common pattern is to have separate Logger instances for each instance
+ * of an object. But, you still want to distinguish between each instance.
+ * Since Log.repository.getLogger() returns shared Logger objects,
+ * monkeypatching one Logger modifies them all.
+ *
+ * This function returns a new object with a prototype chain that chains
+ * up to the original Logger instance. The new prototype has log functions
+ * that prefix content to each message.
+ *
+ * @param name
+ * (string) The Logger to retrieve.
+ * @param prefix
+ * (string) The string to prefix each logged message with.
+ */
+ getLoggerWithMessagePrefix(name, prefix) {
+ let log = this.getLogger(name);
+
+ let proxy = Object.create(log);
+ proxy.log = (level, string, params) => {
+ if (Array.isArray(string) && Array.isArray(params)) {
+ // Template literal.
+ // We cannot change the original array, so create a new one.
+ string = [prefix + string[0]].concat(string.slice(1));
+ } else {
+ string = prefix + string; // Regular string.
+ }
+ return log.log(level, string, params);
+ };
+ return proxy;
+ }
+}
+
+/*
+ * Formatters
+ * These massage a LogMessage into whatever output is desired.
+ */
+
+// Basic formatter that doesn't do anything fancy.
+class BasicFormatter {
+ constructor(dateFormat) {
+ if (dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+ this.parameterFormatter = new ParameterFormatter();
+ }
+
+ /**
+ * Format the text of a message with optional parameters.
+ * If the text contains ${identifier}, replace that with
+ * the value of params[identifier]; if ${}, replace that with
+ * the entire params object. If no params have been substituted
+ * into the text, format the entire object and append that
+ * to the message.
+ */
+ formatText(message) {
+ let params = message.params;
+ if (typeof params == "undefined") {
+ return message.message || "";
+ }
+ // Defensive handling of non-object params
+ // We could add a special case for NSRESULT values here...
+ let pIsObject = typeof params == "object" || typeof params == "function";
+
+ // if we have params, try and find substitutions.
+ if (this.parameterFormatter) {
+ // have we successfully substituted any parameters into the message?
+ // in the log message
+ let subDone = false;
+ let regex = /\$\{(\S*?)\}/g;
+ let textParts = [];
+ if (message.message) {
+ textParts.push(
+ message.message.replace(regex, (_, sub) => {
+ // ${foo} means use the params['foo']
+ if (sub) {
+ if (pIsObject && sub in message.params) {
+ subDone = true;
+ return this.parameterFormatter.format(message.params[sub]);
+ }
+ return "${" + sub + "}";
+ }
+ // ${} means use the entire params object.
+ subDone = true;
+ return this.parameterFormatter.format(message.params);
+ })
+ );
+ }
+ if (!subDone) {
+ // There were no substitutions in the text, so format the entire params object
+ let rest = this.parameterFormatter.format(message.params);
+ if (rest !== null && rest != "{}") {
+ textParts.push(rest);
+ }
+ }
+ return textParts.join(": ");
+ }
+ return undefined;
+ }
+
+ format(message) {
+ return (
+ message.time +
+ "\t" +
+ message.loggerName +
+ "\t" +
+ message.levelDesc +
+ "\t" +
+ this.formatText(message)
+ );
+ }
+}
+
+/**
+ * Test an object to see if it is a Mozilla JS Error.
+ */
+function isError(aObj) {
+ return (
+ aObj &&
+ typeof aObj == "object" &&
+ "name" in aObj &&
+ "message" in aObj &&
+ "fileName" in aObj &&
+ "lineNumber" in aObj &&
+ "stack" in aObj
+ );
+}
+
+/*
+ * Parameter Formatters
+ * These massage an object used as a parameter for a LogMessage into
+ * a string representation of the object.
+ */
+
+class ParameterFormatter {
+ constructor() {
+ this._name = "ParameterFormatter";
+ }
+
+ format(ob) {
+ try {
+ if (ob === undefined) {
+ return "undefined";
+ }
+ if (ob === null) {
+ return "null";
+ }
+ // Pass through primitive types and objects that unbox to primitive types.
+ if (
+ (typeof ob != "object" || typeof ob.valueOf() != "object") &&
+ typeof ob != "function"
+ ) {
+ return ob;
+ }
+ if (ob instanceof Ci.nsIException) {
+ return `${ob} ${Log.stackTrace(ob)}`;
+ } else if (isError(ob)) {
+ return Log._formatError(ob);
+ }
+ // Just JSONify it. Filter out our internal fields and those the caller has
+ // already handled.
+ return JSON.stringify(ob, (key, val) => {
+ if (INTERNAL_FIELDS.has(key)) {
+ return undefined;
+ }
+ return val;
+ });
+ } catch (e) {
+ dumpError(
+ `Exception trying to format object for log message: ${Log.exceptionStr(
+ e
+ )}`
+ );
+ }
+ // Fancy formatting failed. Just toSource() it - but even this may fail!
+ try {
+ return ob.toSource();
+ } catch (_) {}
+ try {
+ return String(ob);
+ } catch (_) {
+ return "[object]";
+ }
+ }
+}
+
+/*
+ * Appenders
+ * These can be attached to Loggers to log to different places
+ * Simply subclass and override doAppend to implement a new one
+ */
+
+class Appender {
+ constructor(formatter) {
+ this.level = Log.Level.All;
+ this._name = "Appender";
+ this._formatter = formatter || new BasicFormatter();
+ }
+
+ append(message) {
+ if (message) {
+ this.doAppend(this._formatter.format(message));
+ }
+ }
+
+ toString() {
+ return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
+ }
+}
+
+/*
+ * DumpAppender
+ * Logs to standard out
+ */
+
+class DumpAppender extends Appender {
+ constructor(formatter) {
+ super(formatter);
+ this._name = "DumpAppender";
+ }
+
+ doAppend(formatted) {
+ dump(formatted + "\n");
+ }
+}
+
+/*
+ * ConsoleAppender
+ * Logs to the javascript console
+ */
+
+class ConsoleAppender extends Appender {
+ constructor(formatter) {
+ super(formatter);
+ this._name = "ConsoleAppender";
+ }
+
+ // XXX this should be replaced with calls to the Browser Console
+ append(message) {
+ if (message) {
+ let m = this._formatter.format(message);
+ if (message.level > Log.Level.Warn) {
+ // TODO: Bug 1801091 - Figure out how to replace this.
+ // eslint-disable-next-line mozilla/no-cu-reportError
+ Cu.reportError(m);
+ return;
+ }
+ this.doAppend(m);
+ }
+ }
+
+ doAppend(formatted) {
+ Services.console.logStringMessage(formatted);
+ }
+}
+
+Object.assign(Log, {
+ LogMessage,
+ Logger,
+ LoggerRepository,
+
+ BasicFormatter,
+
+ Appender,
+ DumpAppender,
+ ConsoleAppender,
+
+ ParameterFormatter,
+});