Bug 1480327: Part 2 - Modernize what's left of Log.jsm a bit. r?Mossop draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 01 Aug 2018 23:23:34 -0700
changeset 825741 e42c2933d26828bfbb2d5cefed63da4454162aaa
parent 825740 65c94be2b7473940496285d354959b30eff5167c
push id118157
push usermaglione.k@gmail.com
push dateThu, 02 Aug 2018 07:02:10 +0000
reviewersMossop
bugs1480327
milestone63.0a1
Bug 1480327: Part 2 - Modernize what's left of Log.jsm a bit. r?Mossop MozReview-Commit-ID: H06rpiZuIEF
mobile/android/modules/geckoview/GeckoViewUtils.jsm
services/common/logmanager.js
toolkit/modules/Log.jsm
toolkit/modules/tests/xpcshell/test_Log.js
--- a/mobile/android/modules/geckoview/GeckoViewUtils.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewUtils.jsm
@@ -9,67 +9,59 @@ XPCOMUtils.defineLazyModuleGetters(this,
   AndroidLog: "resource://gre/modules/AndroidLog.jsm",
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
   Log: "resource://gre/modules/Log.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
 var EXPORTED_SYMBOLS = ["GeckoViewUtils"];
 
-var {Appender, BasicFormatter} = Log;
-
 /**
  * A formatter that does not prepend time/name/level information to messages,
  * because those fields are logged separately when using the Android logger.
  */
-function AndroidFormatter() {
-  BasicFormatter.call(this);
-}
-AndroidFormatter.prototype = Object.freeze({
-  __proto__: BasicFormatter.prototype,
-
+class AndroidFormatter extends Log.BasicFormatter {
   format(message) {
     return this.formatText(message);
-  },
-});
+  }
+}
 
 /*
  * AndroidAppender
  * Logs to Android logcat using AndroidLog.jsm
  */
-function AndroidAppender(aFormatter) {
-  Appender.call(this, aFormatter || new AndroidFormatter());
-  this._name = "AndroidAppender";
-}
-AndroidAppender.prototype = {
-  __proto__: Appender.prototype,
+class AndroidAppender extends Log.Appender {
+  constructor(aFormatter) {
+    super(aFormatter || new AndroidFormatter());
+    this._name = "AndroidAppender";
 
-  // Map log level to AndroidLog.foo method.
-  _mapping: {
-    [Log.Level.Fatal]:  "e",
-    [Log.Level.Error]:  "e",
-    [Log.Level.Warn]:   "w",
-    [Log.Level.Info]:   "i",
-    [Log.Level.Config]: "d",
-    [Log.Level.Debug]:  "d",
-    [Log.Level.Trace]:  "v",
-  },
+    // Map log level to AndroidLog.foo method.
+    this._mapping = {
+      [Log.Level.Fatal]:  "e",
+      [Log.Level.Error]:  "e",
+      [Log.Level.Warn]:   "w",
+      [Log.Level.Info]:   "i",
+      [Log.Level.Config]: "d",
+      [Log.Level.Debug]:  "d",
+      [Log.Level.Trace]:  "v",
+    };
+  }
 
   append(aMessage) {
     if (!aMessage) {
       return;
     }
 
     // AndroidLog.jsm always prepends "Gecko" to the tag, so we strip any
     // leading "Gecko" here. Also strip dots to save space.
     const tag = aMessage.loggerName.replace(/^Gecko|\./g, "");
     const msg = this._formatter.format(aMessage);
     AndroidLog[this._mapping[aMessage.level]](tag, msg);
-  },
-};
+  }
+}
 
 var GeckoViewUtils = {
   /**
    * Define a lazy getter that loads an object from external code, and
    * optionally handles observer and/or message manager notifications for the
    * object, so the object only loads when a notification is received.
    *
    * @param scope     Scope for holding the loaded object.
--- a/services/common/logmanager.js
+++ b/services/common/logmanager.js
@@ -37,45 +37,41 @@ const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 
 // Singletons used by each instance.
 var formatter;
 var dumpAppender;
 var consoleAppender;
 
 // A set of all preference roots used by all instances.
 var allBranches = new Set();
 
-let {Appender} = Log;
-
 const ONE_BYTE = 1;
 const ONE_KILOBYTE = 1024 * ONE_BYTE;
 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
 
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
 /**
  * Append to an nsIStorageStream
  *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
  * during logging. Instead, one can periodically consume the input stream and
  * e.g. write it to disk asynchronously.
  */
-function StorageStreamAppender(formatter) {
-  Appender.call(this, formatter);
-  this._name = "StorageStreamAppender";
-}
+class StorageStreamAppender extends Log.Appender {
+  constructor(formatter) {
+    super(formatter);
+    this._name = "StorageStreamAppender";
 
-StorageStreamAppender.prototype = {
-  __proto__: Appender.prototype,
+    this._converterStream = null; // holds the nsIConverterOutputStream
+    this._outputStream = null; // holds the underlying nsIOutputStream
 
-  _converterStream: null, // holds the nsIConverterOutputStream
-  _outputStream: null,    // holds the underlying nsIOutputStream
-
-  _ss: null,
+    this._ss = null;
+  }
 
   get outputStream() {
     if (!this._outputStream) {
       // First create a raw stream. We can bail out early if that fails.
       this._outputStream = this.newOutputStream();
       if (!this._outputStream) {
         return null;
       }
@@ -84,40 +80,40 @@ StorageStreamAppender.prototype = {
       // the instance if we already have one.
       if (!this._converterStream) {
         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                                   .createInstance(Ci.nsIConverterOutputStream);
       }
       this._converterStream.init(this._outputStream, "UTF-8");
     }
     return this._converterStream;
-  },
+  }
 
-  newOutputStream: function newOutputStream() {
+  newOutputStream() {
     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
                           .createInstance(Ci.nsIStorageStream);
     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
     return ss.getOutputStream(0);
-  },
+  }
 
-  getInputStream: function getInputStream() {
+  getInputStream() {
     if (!this._ss) {
       return null;
     }
     return this._ss.newInputStream(0);
-  },
+  }
 
-  reset: function reset() {
+  reset() {
     if (!this._outputStream) {
       return;
     }
     this.outputStream.close();
     this._outputStream = null;
     this._ss = null;
-  },
+  }
 
   doAppend(formatted) {
     if (!formatted) {
       return;
     }
     try {
       this.outputStream.writeString(formatted + "\n");
     } catch (ex) {
@@ -127,41 +123,39 @@ StorageStreamAppender.prototype = {
         this._outputStream = null;
       } try {
           this.outputStream.writeString(formatted + "\n");
       } catch (ex) {
         // Ah well, we tried, but something seems to be hosed permanently.
       }
     }
   }
-};
+}
 
 // A storage appender that is flushable to a file on disk.  Policies for
 // when to flush, to what file, log rotation etc are up to the consumer
 // (although it does maintain a .sawError property to help the consumer decide
 // based on its policies)
-function FlushableStorageAppender(formatter) {
-  StorageStreamAppender.call(this, formatter);
-  this.sawError = false;
-}
-
-FlushableStorageAppender.prototype = {
-  __proto__: StorageStreamAppender.prototype,
+class FlushableStorageAppender extends StorageStreamAppender {
+  constructor(formatter) {
+    super(formatter);
+    this.sawError = false;
+  }
 
   append(message) {
     if (message.level >= Log.Level.Error) {
       this.sawError = true;
     }
     StorageStreamAppender.prototype.append.call(this, message);
-  },
+  }
 
   reset() {
-    Log.StorageStreamAppender.prototype.reset.call(this);
+    super.reset();
     this.sawError = false;
-  },
+  }
 
   // Flush the current stream to a file. Somewhat counter-intuitively, you
   // must pass a log which will be written to with details of the operation.
   async flushToFile(subdirArray, filename, log) {
     let inStream = this.getInputStream();
     this.reset();
     if (!inStream) {
       log.debug("Failed to flush log to a file - no input stream");
@@ -170,17 +164,17 @@ FlushableStorageAppender.prototype = {
     log.debug("Flushing file log");
     log.trace("Beginning stream copy to " + filename + ": " + Date.now());
     try {
       await this._copyStreamToFile(inStream, subdirArray, filename, log);
       log.trace("onCopyComplete", Date.now());
     } catch (ex) {
       log.error("Failed to copy log stream to file", ex);
     }
-  },
+  }
 
   /**
    * Copy an input stream to the named file, doing everything off the main
    * thread.
    * subDirArray is an array of path components, relative to the profile
    * directory, where the file will be created.
    * outputFileName is the filename to create.
    * Returns a promise that is resolved on completion or rejected with an error.
@@ -211,18 +205,18 @@ FlushableStorageAppender.prototype = {
       try {
         binaryStream.close(); // inputStream is closed by the binaryStream
         await output.close();
       } catch (ex) {
         log.error("Failed to close the input stream", ex);
       }
     }
     log.trace("finished copy to", fullOutputFileName);
-  },
-};
+  }
+}
 
 // The public LogManager object.
 function LogManager(prefRoot, logNames, logFilePrefix) {
   this._prefObservers = [];
   this.init(prefRoot, logNames, logFilePrefix);
 }
 
 LogManager.StorageStreamAppender = StorageStreamAppender;
--- a/toolkit/modules/Log.jsm
+++ b/toolkit/modules/Log.jsm
@@ -59,60 +59,48 @@ var Log = {
     Log.repository = new LoggerRepository();
     return Log.repository;
   },
   set repository(value) {
     delete Log.repository;
     Log.repository = value;
   },
 
-  LogMessage,
-  Logger,
-  LoggerRepository,
-
-  BasicFormatter,
-
-  Appender,
-  DumpAppender,
-  ConsoleAppender,
-
-  ParameterFormatter,
-
-  _formatError: function _formatError(e) {
-    let result = e.toString();
+  _formatError(e) {
+    let result = String(e);
     if (e.fileName) {
-      result +=  " (" + e.fileName;
+      let loc = [e.fileName];
       if (e.lineNumber) {
-        result += ":" + e.lineNumber;
+        loc.push(e.lineNumber);
       }
       if (e.columnNumber) {
-        result += ":" + e.columnNumber;
+        loc.push(e.columnNumber);
       }
-      result += ")";
+      result += `(${loc.join(":")})`;
     }
-    return result + " " + Log.stackTrace(e);
+    return `${result} ${Log.stackTrace(e)}`;
   },
 
   // This is for back compatibility with services/common/utils.js; we duplicate
   // some of the logic in ParameterFormatter
-  exceptionStr: function exceptionStr(e) {
+  exceptionStr(e) {
     if (!e) {
-      return "" + e;
+      return String(e);
     }
     if (e instanceof Ci.nsIException) {
-      return e.toString() + " " + Log.stackTrace(e);
+      return `${e} ${Log.stackTrace(e)}`;
     } else if (isError(e)) {
       return Log._formatError(e);
     }
     // else
     let message = e.message || e;
-    return message + " " + Log.stackTrace(e);
+    return `${message} ${Log.stackTrace(e)}`;
   },
 
-  stackTrace: function stackTrace(e) {
+  stackTrace(e) {
     // 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, might be confusing if
         // there are multiple extensions with similar filenames
@@ -131,17 +119,17 @@ var Log = {
           str = frame.name + "()@" + str;
         }
 
         if (str) {
           output.push(str);
         }
         frame = frame.caller;
       }
-      return "Stack trace: " + output.join("\n");
+      return `Stack trace: ${output.join("\n")}`;
     }
     // Standard JS exception
     if (e.stack) {
       let stack = e.stack;
       // Avoid loading Task.jsm if there's no task on the stack.
       if (stack.includes("/Task.jsm:"))
         stack = Task.Debugging.generateReadableStack(stack);
       return "JS Stack trace: " + stack.trim()
@@ -151,80 +139,82 @@ var Log = {
     return "No traceback available";
   }
 };
 
 /*
  * LogMessage
  * Encapsulates a single log event's data
  */
-function LogMessage(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;
+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();
   }
 
-  // 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();
-}
-LogMessage.prototype = {
   get levelDesc() {
     if (this.level in Log.Level.Desc)
       return Log.Level.Desc[this.level];
     return "UNKNOWN";
-  },
+  }
 
-  toString: function LogMsg_toString() {
-    let msg = "LogMessage [" + this.time + " " + this.level + " " +
-      this.message;
+  toString() {
+    let msg = `${this.time} ${this.level} ${this.message}`;
     if (this.params) {
-      msg += " " + JSON.stringify(this.params);
+      msg += ` ${JSON.stringify(this.params)}`;
     }
-    return msg + "]";
+    return `LogMessage [${msg}]`;
   }
-};
+}
 
 /*
  * Logger
  * Hierarchical version.  Logs to all appenders, assigned or inherited
  */
 
-function Logger(name, repository) {
-  if (!repository)
-    repository = Log.repository;
-  this._name = name;
-  this.children = [];
-  this.ownAppenders = [];
-  this.appenders = [];
-  this._repository = repository;
-}
-Logger.prototype = {
-  _levelPrefName: null,
-  _levelPrefValue: null,
+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;
-  },
+  }
 
-  _level: null,
   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) {
@@ -240,97 +230,96 @@ Logger.prototype = {
       }
     }
     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;
-  },
+  }
 
-  _parent: null,
   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: function updateAppenders() {
+  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: function Logger_addAppender(appender) {
+  addAppender(appender) {
     if (this.ownAppenders.includes(appender)) {
       return;
     }
     this.ownAppenders.push(appender);
     this.updateAppenders();
-  },
+  }
 
-  removeAppender: function Logger_removeAppender(appender) {
+  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)) {
@@ -350,17 +339,17 @@ Logger.prototype = {
       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;
@@ -370,63 +359,64 @@ Logger.prototype = {
         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
  */
 
-function LoggerRepository() {}
-LoggerRepository.prototype = {
-  _loggers: {},
+class LoggerRepository {
+  constructor() {
+    this._loggers = {};
+    this._rootLogger = null;
+  }
 
-  _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 "Cannot change the root logger";
-  },
+  }
 
-  _updateParents: function LogRep__updateParents(name) {
+  _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)
@@ -443,34 +433,34 @@ LoggerRepository.prototype = {
     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.
@@ -494,32 +484,33 @@ LoggerRepository.prototype = {
         // 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.
-function BasicFormatter(dateFormat) {
-  if (dateFormat) {
-    this.dateFormat = dateFormat;
+class BasicFormatter {
+  constructor(dateFormat) {
+    if (dateFormat) {
+      this.dateFormat = dateFormat;
+    }
+    this.parameterFormatter = new ParameterFormatter();
   }
-  this.parameterFormatter = new ParameterFormatter();
-}
-BasicFormatter.prototype = {
+
   /**
    * 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.
    */
@@ -559,146 +550,159 @@ BasicFormatter.prototype = {
         let rest = this.parameterFormatter.format(message.params);
         if (rest !== null && rest != "{}") {
           textParts.push(rest);
         }
       }
       return textParts.join(": ");
     }
     return undefined;
-  },
+  }
 
-  format: function BF_format(message) {
+  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.
  */
 
-function ParameterFormatter() {
-  this._name = "ParameterFormatter";
-}
-ParameterFormatter.prototype = {
+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.toString() + " " + Log.stackTrace(ob);
+        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));
+      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 "" + ob;
+      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
  */
 
-function Appender(formatter) {
-  this._name = "Appender";
-  this._formatter = formatter || new BasicFormatter();
-}
-Appender.prototype = {
-  level: Log.Level.All,
+class Appender {
+  constructor(formatter) {
+    this.level = Log.Level.All;
+    this._name = "Appender";
+    this._formatter = formatter || new BasicFormatter();
+  }
 
-  append: function App_append(message) {
+  append(message) {
     if (message) {
       this.doAppend(this._formatter.format(message));
     }
-  },
-  toString: function App_toString() {
-    return this._name + " [level=" + this.level +
-      ", formatter=" + this._formatter + "]";
-  },
-};
+  }
+
+  toString() {
+    return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
+  }
+}
 
 /*
  * DumpAppender
  * Logs to standard out
  */
 
-function DumpAppender(formatter) {
-  Appender.call(this, formatter);
-  this._name = "DumpAppender";
-}
-DumpAppender.prototype = {
-  __proto__: Appender.prototype,
+class DumpAppender extends Appender {
+  constructor(formatter) {
+    super(formatter);
+    this._name = "DumpAppender";
+  }
 
-  doAppend: function DApp_doAppend(formatted) {
+  doAppend(formatted) {
     dump(formatted + "\n");
   }
-};
+}
 
 /*
  * ConsoleAppender
  * Logs to the javascript console
  */
 
-function ConsoleAppender(formatter) {
-  Appender.call(this, formatter);
-  this._name = "ConsoleAppender";
-}
-ConsoleAppender.prototype = {
-  __proto__: Appender.prototype,
+class ConsoleAppender extends Appender {
+  constructor(formatter) {
+    super(formatter);
+    this._name = "ConsoleAppender";
+  }
 
   // XXX this should be replaced with calls to the Browser Console
-  append: function App_append(message) {
+  append(message) {
     if (message) {
       let m = this._formatter.format(message);
       if (message.level > Log.Level.Warn) {
         Cu.reportError(m);
         return;
       }
       this.doAppend(m);
     }
-  },
+  }
 
-  doAppend: function CApp_doAppend(formatted) {
+  doAppend(formatted) {
     Services.console.logStringMessage(formatted);
   }
-};
+}
+
+Object.assign(Log, {
+  LogMessage,
+  Logger,
+  LoggerRepository,
+
+  BasicFormatter,
+
+  Appender,
+  DumpAppender,
+  ConsoleAppender,
+
+  ParameterFormatter,
+});
--- a/toolkit/modules/tests/xpcshell/test_Log.js
+++ b/toolkit/modules/tests/xpcshell/test_Log.js
@@ -11,27 +11,26 @@ ChromeUtils.import("resource://gre/modul
 var testFormatter = {
   format: function format(message) {
     return message.loggerName + "\t" +
       message.levelDesc + "\t" +
       message.message;
   }
 };
 
-function MockAppender(formatter) {
-  Log.Appender.call(this, formatter);
-  this.messages = [];
-}
-MockAppender.prototype = {
-  __proto__: Log.Appender.prototype,
+class MockAppender extends Log.Appender {
+  constructor(formatter) {
+    super(formatter);
+    this.messages = [];
+  }
 
-  doAppend: function DApp_doAppend(message) {
+  doAppend(message) {
     this.messages.push(message);
   }
-};
+}
 
 add_task(function test_Logger() {
   let log = Log.repository.getLogger("test.logger");
   let appender = new MockAppender(new Log.BasicFormatter());
 
   log.level = Log.Level.Debug;
   appender.level = Log.Level.Info;
   log.addAppender(appender);