Bug 1480327: Part 2 - Modernize what's left of Log.jsm a bit. r=Mossop
authorKris Maglione <maglione.k@gmail.com>
Wed, 01 Aug 2018 23:23:34 -0700
changeset 430462 af213f409e578dd4443962e4d60cb7f384103305
parent 430461 ffcc2ae2ef834a3ebc36d18b35c5f2156dd04843
child 430463 8a1c25fdedf62d20563fdd48fd39873f9625a5d8
push id34406
push userncsoregi@mozilla.com
push dateWed, 08 Aug 2018 09:58:58 +0000
treeherdermozilla-central@17116905bc07 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop
bugs1480327
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);