Bug 989137 - Part 3: Log.jsm API to get a Logger that prefixes messages; r=bsmedberg
authorGregory Szorc <gps@mozilla.com>
Fri, 28 Mar 2014 11:36:37 -0700
changeset 194644 5b408704c9987e5a8d2a482e5ee9c8ebfbba6032
parent 194643 0857d467b4ed341d82338e5da8caf4b89a02a27d
child 194645 c27967f904b68d370e18ba4cabc16d74c170ede4
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbsmedberg
bugs989137
milestone31.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 989137 - Part 3: Log.jsm API to get a Logger that prefixes messages; r=bsmedberg A common pattern for logging is to have multiple loggers for multiple underlying object instances. You often want to have each instance attach some identifying metdata contained in each logged message. This patch provides an API to facilitate that.
toolkit/modules/Log.jsm
toolkit/modules/Sqlite.jsm
toolkit/modules/tests/xpcshell/test_Log.js
--- a/toolkit/modules/Log.jsm
+++ b/toolkit/modules/Log.jsm
@@ -64,16 +64,17 @@ this.Log = {
   },
 
   LogMessage: LogMessage,
   Logger: Logger,
   LoggerRepository: LoggerRepository,
 
   Formatter: Formatter,
   BasicFormatter: BasicFormatter,
+  MessageOnlyFormatter: MessageOnlyFormatter,
   StructuredFormatter: StructuredFormatter,
 
   Appender: Appender,
   DumpAppender: DumpAppender,
   ConsoleAppender: ConsoleAppender,
   StorageStreamAppender: StorageStreamAppender,
 
   FileAppender: FileAppender,
@@ -354,23 +355,68 @@ LoggerRepository.prototype = {
 
     // 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);
     }
   },
 
-  getLogger: function LogRep_getLogger(name) {
+  /**
+   * 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: function (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: function (name, prefix) {
+    let log = this.getLogger(name);
+
+    let proxy = {__proto__: log};
+
+    for (let level in Log.Level) {
+      if (level == "Desc") {
+        continue;
+      }
+
+      let lc = level.toLowerCase();
+      proxy[lc] = function (msg, ...args) {
+        return log[lc].apply(log, [prefix + msg, ...args]);
+      };
+    }
+
+    return proxy;
+  },
 };
 
 /*
  * Formatters
  * These massage a LogMessage into whatever output is desired.
  * BasicFormatter and StructuredFormatter are implemented here.
  */
 
@@ -391,16 +437,29 @@ BasicFormatter.prototype = {
   format: function BF_format(message) {
     return message.time + "\t" +
       message.loggerName + "\t" +
       message.levelDesc + "\t" +
       message.message + "\n";
   }
 };
 
+/**
+ * A formatter that only formats the string message component.
+ */
+function MessageOnlyFormatter() {
+}
+MessageOnlyFormatter.prototype = Object.freeze({
+  __proto__: Formatter.prototype,
+
+  format: function (message) {
+    return message.message + "\n";
+  },
+});
+
 // Structured formatter that outputs JSON based on message data.
 // This formatter will format unstructured messages by supplying
 // default values.
 function StructuredFormatter() { }
 StructuredFormatter.prototype = {
   __proto__: Formatter.prototype,
 
   format: function (logMessage) {
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -168,38 +168,18 @@ function openConnection(options) {
  *        (string) The basename of this database name. Used for logging.
  * @param number
  *        (Number) The connection number to this database.
  * @param options
  *        (object) Options to control behavior of connection. See
  *        `openConnection`.
  */
 function OpenedConnection(connection, basename, number, options) {
-  let log = Log.repository.getLogger("Sqlite.Connection." + basename);
-
-  // getLogger() returns a shared object. We can't modify the functions on this
-  // object since they would have effect on all instances and last write would
-  // win. So, we create a "proxy" object with our custom functions. Everything
-  // else is proxied back to the shared logger instance via prototype
-  // inheritance.
-  let logProxy = {__proto__: log};
-
-  // Automatically prefix all log messages with the identifier.
-  for (let level in Log.Level) {
-    if (level == "Desc") {
-      continue;
-    }
-
-    let lc = level.toLowerCase();
-    logProxy[lc] = function (msg) {
-      return log[lc].call(log, "Conn #" + number + ": " + msg);
-    };
-  }
-
-  this._log = logProxy;
+  this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
+                                                        "Conn #" + number + ": ");
 
   this._log.info("Opened");
 
   this._connection = connection;
   this._connectionIdentifier = basename + " Conn #" + number;
   this._open = true;
 
   this._cachedStatements = new Map();
--- a/toolkit/modules/tests/xpcshell/test_Log.js
+++ b/toolkit/modules/tests/xpcshell/test_Log.js
@@ -67,16 +67,36 @@ add_test(function test_Logger_parent() {
   Log.repository.rootLogger.info("this shouldn't show up in gpAppender");
 
   do_check_eq(gpAppender.messages.length, 1);
   do_check_true(gpAppender.messages[0].indexOf("child info test") > 0);
 
   run_next_test();
 });
 
+add_test(function test_LoggerWithMessagePrefix() {
+  let log = Log.repository.getLogger("test.logger.prefix");
+  let appender = new MockAppender(new Log.MessageOnlyFormatter());
+  log.addAppender(appender);
+
+  let prefixed = Log.repository.getLoggerWithMessagePrefix(
+    "test.logger.prefix", "prefix: ");
+
+  log.warn("no prefix");
+  prefixed.warn("with prefix");
+
+  Assert.equal(appender.messages.length, 2, "2 messages were logged.");
+  Assert.deepEqual(appender.messages, [
+    "no prefix\n",
+    "prefix: with prefix\n",
+  ], "Prefix logger works.");
+
+  run_next_test();
+});
+
 // A utility method for checking object equivalence.
 // Fields with a reqular expression value in expected will be tested
 // against the corresponding value in actual. Otherwise objects
 // are expected to have the same keys and equal values.
 function checkObjects(expected, actual) {
   do_check_true(expected instanceof Object);
   do_check_true(actual instanceof Object);
   for (let key in expected) {