Bug 1168872 - Support for server side logging; r=panos, ochameau
authorJan Odvarko <odvarko@gmail.com>
Mon, 17 Aug 2015 12:48:01 +0200
changeset 258170 28077c310d985f70241ab37f30e55dfbf94b1c64
parent 258169 40b5df5cdc590a00c484033dd253e2be3a76249e
child 258171 698d20c94746543bcfece4766a440e0d78f90e85
push id63843
push userryanvm@gmail.com
push dateTue, 18 Aug 2015 14:58:06 +0000
treeherdermozilla-inbound@d55e24c983aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspanos, ochameau
bugs1168872
milestone43.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 1168872 - Support for server side logging; r=panos, ochameau
browser/app/profile/firefox.js
browser/devtools/webconsole/console-output.js
browser/devtools/webconsole/test/browser.ini
browser/devtools/webconsole/test/browser_console_server_logging.js
browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
browser/devtools/webconsole/test/head.js
browser/devtools/webconsole/test/test-console-server-logging.sjs
browser/devtools/webconsole/webconsole.js
browser/devtools/webconsole/webconsole.xul
browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
browser/themes/shared/devtools/webconsole.inc.css
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/webconsole/moz.build
toolkit/devtools/webconsole/server-logger-monitor.js
toolkit/devtools/webconsole/server-logger.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1557,16 +1557,20 @@ pref("devtools.webconsole.filter.error",
 pref("devtools.webconsole.filter.warn", true);
 pref("devtools.webconsole.filter.info", true);
 pref("devtools.webconsole.filter.log", true);
 pref("devtools.webconsole.filter.secerror", true);
 pref("devtools.webconsole.filter.secwarn", true);
 pref("devtools.webconsole.filter.serviceworkers", false);
 pref("devtools.webconsole.filter.sharedworkers", false);
 pref("devtools.webconsole.filter.windowlessworkers", false);
+pref("devtools.webconsole.filter.servererror", false);
+pref("devtools.webconsole.filter.serverwarn", false);
+pref("devtools.webconsole.filter.serverinfo", false);
+pref("devtools.webconsole.filter.serverlog", false);
 
 // Remember the Browser Console filters
 pref("devtools.browserconsole.filter.network", true);
 pref("devtools.browserconsole.filter.networkinfo", false);
 pref("devtools.browserconsole.filter.netwarn", true);
 pref("devtools.browserconsole.filter.netxhr", false);
 pref("devtools.browserconsole.filter.csserror", true);
 pref("devtools.browserconsole.filter.cssparser", false);
@@ -1578,16 +1582,20 @@ pref("devtools.browserconsole.filter.err
 pref("devtools.browserconsole.filter.warn", true);
 pref("devtools.browserconsole.filter.info", true);
 pref("devtools.browserconsole.filter.log", true);
 pref("devtools.browserconsole.filter.secerror", true);
 pref("devtools.browserconsole.filter.secwarn", true);
 pref("devtools.browserconsole.filter.serviceworkers", true);
 pref("devtools.browserconsole.filter.sharedworkers", true);
 pref("devtools.browserconsole.filter.windowlessworkers", true);
+pref("devtools.browserconsole.filter.servererror", false);
+pref("devtools.browserconsole.filter.serverwarn", false);
+pref("devtools.browserconsole.filter.serverinfo", false);
+pref("devtools.browserconsole.filter.serverlog", false);
 
 // Text size in the Web Console. Use 0 for the system default size.
 pref("devtools.webconsole.fontSize", 0);
 
 // Max number of inputs to store in web console history.
 pref("devtools.webconsole.inputHistoryCount", 50);
 
 // Persistent logging: |true| if you want the Web Console to keep all of the
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -39,16 +39,17 @@ const COMPAT = {
   CATEGORIES: {
     NETWORK: 0,
     CSS: 1,
     JS: 2,
     WEBDEV: 3,
     INPUT: 4,
     OUTPUT: 5,
     SECURITY: 6,
+    SERVER: 7,
   },
 
   // The possible message severities.
   SEVERITIES: {
     ERROR: 0,
     WARNING: 1,
     INFO: 2,
     LOG: 3,
@@ -63,21 +64,22 @@ const COMPAT = {
     // Error        Warning       Info    Log
     [ "network",    "netwarn",    null,   "networkinfo", ],  // Network
     [ "csserror",   "cssparser",  null,   null,          ],  // CSS
     [ "exception",  "jswarn",     null,   "jslog",       ],  // JS
     [ "error",      "warn",       "info", "log",         ],  // Web Developer
     [ null,         null,         null,   null,          ],  // Input
     [ null,         null,         null,   null,          ],  // Output
     [ "secerror",   "secwarn",    null,   null,          ],  // Security
+    [ "servererror", "serverwarn", "serverinfo", "serverlog",   ],  // Server Logging
   ],
 
   // The fragment of a CSS class name that identifies each category.
   CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
-                              "input", "output", "security" ],
+                              "input", "output", "security", "server" ],
 
   // The fragment of a CSS class name that identifies each severity.
   SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
 
   // The indent of a console group in pixels.
   GROUP_INDENT: 12,
 };
 
@@ -1295,17 +1297,17 @@ Messages.JavaScriptEvalOutput.prototype 
  * @param object packet
  *        The Console API call packet received from the server.
  */
 Messages.ConsoleGeneric = function(packet)
 {
   let options = {
     className: "cm-s-mozilla",
     timestamp: packet.timeStamp,
-    category: "webdev",
+    category: packet.category || "webdev",
     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
     prefix: packet.prefix,
     private: packet.private,
     filterDuplicates: true,
     location: {
       url: packet.filename,
       line: packet.lineNumber,
       column: packet.columnNumber
@@ -1566,17 +1568,17 @@ Messages.ConsoleGeneric.prototype = Heri
  * @param object packet
  *        The Console API call packet received from the server.
  */
 Messages.ConsoleTrace = function(packet)
 {
   let options = {
     className: "cm-s-mozilla",
     timestamp: packet.timeStamp,
-    category: "webdev",
+    category: packet.category || "webdev",
     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
     private: packet.private,
     filterDuplicates: true,
     location: {
       url: packet.filename,
       line: packet.lineNumber,
     },
   };
@@ -1703,17 +1705,17 @@ Messages.ConsoleTrace.prototype = Herita
  * @param object packet
  *        The Console API call packet received from the server.
  */
 Messages.ConsoleTable = function(packet)
 {
   let options = {
     className: "cm-s-mozilla",
     timestamp: packet.timeStamp,
-    category: "webdev",
+    category: packet.category || "webdev",
     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
     private: packet.private,
     filterDuplicates: false,
     location: {
       url: packet.filename,
       line: packet.lineNumber,
     },
   };
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -68,16 +68,17 @@ support-files =
   test-bug-869003-top-window.html
   test-closure-optimized-out.html
   test-closures.html
   test-console-assert.html
   test-console-count.html
   test-console-count-external-file.js
   test-console-extras.html
   test-console-replaced-api.html
+  test-console-server-logging.sjs
   test-console.html
   test-console-workers.html
   test-console-table.html
   test-console-output-02.html
   test-console-output-03.html
   test-console-output-04.html
   test-console-output-dom-elements.html
   test-console-output-events.html
@@ -167,16 +168,17 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_console_native_getters.js]
 [browser_console_navigation_marker.js]
 [browser_console_nsiconsolemessage.js]
 skip-if = buildapp == 'mulet'
 [browser_console_optimized_out_vars.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_console_private_browsing.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
+[browser_console_server_logging.js]
 [browser_console_variables_view.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_console_variables_view_filter.js]
 [browser_console_variables_view_dom_nodes.js]
 [browser_console_variables_view_dont_sort_non_sortable_classes_properties.js]
 skip-if = buildapp == 'mulet'
 [browser_console_variables_view_while_debugging.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_server_logging.js
@@ -0,0 +1,34 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+"use strict";
+
+// Check that server log appears in the console panel - bug 1168872
+let test = asyncTest(function* () {
+  const PREF = "devtools.webconsole.filter.serverlog";
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-server-logging.sjs";
+
+  Services.prefs.setBoolPref(PREF, true);
+  registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+  yield loadTab(TEST_URI);
+
+  let hud = yield openConsole();
+
+  BrowserReload();
+
+  // Note that the test is also checking out the (printf like)
+  // formatters and encoding of UTF8 characters (see the one at the end).
+  let text = "values: string  Object { a: 10 }  123 1.12 \u2713";
+
+  yield waitForMessages({
+    webconsole: hud,
+    messages: [{
+      text: text,
+      category: CATEGORY_SERVER,
+      severity: SEVERITY_LOG,
+    }],
+  })
+});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -22,22 +22,24 @@ let test = asyncTest(function* () {
 });
 
 function testFilterButtons() {
   testMenuFilterButton("net");
   testMenuFilterButton("css");
   testMenuFilterButton("js");
   testMenuFilterButton("logging");
   testMenuFilterButton("security");
+  testMenuFilterButton("server");
 
   testIsolateFilterButton("net");
   testIsolateFilterButton("css");
   testIsolateFilterButton("js");
   testIsolateFilterButton("logging");
   testIsolateFilterButton("security");
+  testIsolateFilterButton("server");
 }
 
 function testMenuFilterButton(category) {
   let selector = ".webconsole-filter-button[category=\"" + category + "\"]";
   let button = hudBox.querySelector(selector);
   ok(button, "we have the \"" + category + "\" button");
 
   let firstMenuItem = button.querySelector("menuitem");
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -23,16 +23,17 @@ let gPendingOutputTest = 0;
 // The various categories of messages.
 const CATEGORY_NETWORK = 0;
 const CATEGORY_CSS = 1;
 const CATEGORY_JS = 2;
 const CATEGORY_WEBDEV = 3;
 const CATEGORY_INPUT = 4;
 const CATEGORY_OUTPUT = 5;
 const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
 
 // The possible message severities.
 const SEVERITY_ERROR = 0;
 const SEVERITY_WARNING = 1;
 const SEVERITY_INFO = 2;
 const SEVERITY_LOG = 3;
 
 // The indent of a console group in pixels.
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-server-logging.sjs
@@ -0,0 +1,32 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+  var page = "<!DOCTYPE html><html>" +
+    "<head><meta charset='utf-8'></head>" +
+    "<body><p>hello world!</p></body>" +
+    "</html>";
+
+  var data = {
+    "version": "4.1.0",
+    "columns": ["log", "backtrace", "type"],
+    "rows": [[
+      ["values: %s %o %i %f %s","string",{"a":10,"___class_name":"Object"},123,1.12, "\u2713"],
+      "C:\\src\\www\\serverlogging\\test7.php:4:1",
+      ""
+    ]]
+  };
+
+  // Put log into headers.
+  var value = b64EncodeUnicode(JSON.stringify(data));
+  response.setHeader("X-ChromeLogger-Data", value, false);
+
+  response.write(page);
+}
+
+function b64EncodeUnicode(str) {
+  return btoa(unescape(encodeURIComponent(str)));
+}
\ No newline at end of file
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -29,16 +29,17 @@ loader.lazyGetter(this, "Messages",
 loader.lazyGetter(this, "asyncStorage",
                   () => require("devtools/toolkit/shared/async-storage"));
 loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/toolkit/client/main", true);
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/toolkit/client/main", true);
 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
 loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
 loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+loader.lazyGetter(this, "Timers", () => require("sdk/timers"));
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
 
@@ -71,16 +72,17 @@ const DEFAULT_LOG_LIMIT = 1000;
 // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
 const CATEGORY_NETWORK = 0;
 const CATEGORY_CSS = 1;
 const CATEGORY_JS = 2;
 const CATEGORY_WEBDEV = 3;
 const CATEGORY_INPUT = 4;   // always on
 const CATEGORY_OUTPUT = 5;  // always on
 const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
 
 // The possible message severities. As before, we start at zero so we can use
 // these as indexes into MESSAGE_PREFERENCE_KEYS.
 const SEVERITY_ERROR = 0;
 const SEVERITY_WARNING = 1;
 const SEVERITY_INFO = 2;
 const SEVERITY_LOG = 3;
 
@@ -88,40 +90,42 @@ const SEVERITY_LOG = 3;
 const CATEGORY_CLASS_FRAGMENTS = [
   "network",
   "cssparser",
   "exception",
   "console",
   "input",
   "output",
   "security",
+  "server",
 ];
 
 // The fragment of a CSS class name that identifies each severity.
 const SEVERITY_CLASS_FRAGMENTS = [
   "error",
   "warn",
   "info",
   "log",
 ];
 
 // The preference keys to use for each category/severity combination, indexed
 // first by category (rows) and then by severity (columns).
 //
 // Most of these rather idiosyncratic names are historical and predate the
 // division of message type into "category" and "severity".
 const MESSAGE_PREFERENCE_KEYS = [
-//  Error         Warning       Info       Log
-  [ "network",    "netwarn",    "netxhr",  "networkinfo", ],  // Network
-  [ "csserror",   "cssparser",  null,      "csslog",      ],  // CSS
-  [ "exception",  "jswarn",     null,      "jslog",       ],  // JS
-  [ "error",      "warn",       "info",    "log",         ],  // Web Developer
-  [ null,         null,         null,      null,          ],  // Input
-  [ null,         null,         null,      null,          ],  // Output
-  [ "secerror",   "secwarn",    null,      null,          ],  // Security
+//  Error          Warning       Info          Log
+  [ "network",     "netwarn",    "netxhr",     "networkinfo", ],  // Network
+  [ "csserror",    "cssparser",  null,         "csslog",      ],  // CSS
+  [ "exception",   "jswarn",     null,         "jslog",       ],  // JS
+  [ "error",       "warn",       "info",       "log",         ],  // Web Developer
+  [ null,          null,         null,         null,          ],  // Input
+  [ null,          null,         null,         null,          ],  // Output
+  [ "secerror",    "secwarn",    null,         null,          ],  // Security
+  [ "servererror", "serverwarn", "serverinfo", "serverlog",   ],  // Server Logging
 ];
 
 // A mapping from the console API log event levels to the Web Console
 // severities.
 const LEVELS = {
   error: SEVERITY_ERROR,
   exception: SEVERITY_ERROR,
   assert: SEVERITY_ERROR,
@@ -212,16 +216,17 @@ function WebConsoleFrame(aWebConsoleOwne
   this.filterPrefs = {};
 
   this.output = new ConsoleOutput(this);
 
   this._toggleFilter = this._toggleFilter.bind(this);
   this._onPanelSelected = this._onPanelSelected.bind(this);
   this._flushMessageQueue = this._flushMessageQueue.bind(this);
   this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+  this._onUpdateListeners = this._onUpdateListeners.bind(this);
 
   this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   this._outputTimerInitialized = false;
 
   EventEmitter.decorate(this);
 }
 exports.WebConsoleFrame = WebConsoleFrame;
 
@@ -647,46 +652,79 @@ WebConsoleFrame.prototype = {
    * Initialize the default filter preferences.
    * @private
    */
   _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
   {
     let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
                  "exception", "jswarn", "jslog", "error", "info", "warn", "log",
                  "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
-                 "serviceworkers", "windowlessworkers"];
+                 "serviceworkers", "windowlessworkers", "servererror",
+                 "serverwarn", "serverinfo", "serverlog"];
+
     for (let pref of prefs) {
-      this.filterPrefs[pref] = Services.prefs
-                               .getBoolPref(this._filterPrefsPrefix + pref);
+      this.filterPrefs[pref] = Services.prefs.getBoolPref(
+        this._filterPrefsPrefix + pref);
     }
   },
 
   /**
    * Attach / detach reflow listeners depending on the checked status
    * of the `CSS > Log` menuitem.
    *
    * @param function [aCallback=null]
    *        Optional function to invoke when the listener has been
    *        added/removed.
-   *
    */
   _updateReflowActivityListener:
     function WCF__updateReflowActivityListener(aCallback)
   {
     if (this.webConsoleClient) {
       let pref = this._filterPrefsPrefix + "csslog";
       if (Services.prefs.getBoolPref(pref)) {
         this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
       } else {
         this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
       }
     }
   },
 
   /**
+   * Attach / detach server logging listener depending on the filter
+   * preferences. If the user isn't interested in the server logs at
+   * all the listener is not registered.
+   *
+   * @param function [aCallback=null]
+   *        Optional function to invoke when the listener has been
+   *        added/removed.
+   */
+  _updateServerLoggingListener:
+    function WCF__updateServerLoggingListener(aCallback)
+  {
+    if (!this.webConsoleClient) {
+      return;
+    }
+
+    let startListener = false;
+    let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"];
+    for (let i = 0; i < prefs.length; i++) {
+      if (this.filterPrefs[prefs[i]]) {
+        startListener = true;
+        break;
+      }
+    }
+
+    if (startListener) {
+      this.webConsoleClient.startListeners(["ServerLogging"], aCallback);
+    } else {
+      this.webConsoleClient.stopListeners(["ServerLogging"], aCallback);
+    }
+  },
+
+  /**
    * Sets the events for the filter input field.
    * @private
    */
   _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
   {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
 
@@ -748,16 +786,19 @@ WebConsoleFrame.prototype = {
 
     if (Services.appinfo.OS == "Darwin") {
       let net = this.document.querySelector("toolbarbutton[category=net]");
       let accesskey = net.getAttribute("accesskeyMacOSX");
       net.setAttribute("accesskey", accesskey);
 
       let logging = this.document.querySelector("toolbarbutton[category=logging]");
       logging.removeAttribute("accesskey");
+
+      let serverLogging = this.document.querySelector("toolbarbutton[category=server]");
+      serverLogging.removeAttribute("accesskey");
     }
   },
 
   /**
    * Increase, decrease or reset the font size.
    *
    * @param string size
    *        The size of the font change. Accepted values are "+" and "-".
@@ -965,32 +1006,48 @@ WebConsoleFrame.prototype = {
    * @param string aToggleType
    * @param boolean aState
    * @returns void
    */
   setFilterState: function WCF_setFilterState(aToggleType, aState)
   {
     this.filterPrefs[aToggleType] = aState;
     this.adjustVisibilityForMessageType(aToggleType, aState);
+
     Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
-    this._updateReflowActivityListener();
+
+    if (this._updateListenersTimeout) {
+      Timers.clearTimeout(this._updateListenersTimeout);
+    }
+
+    this._updateListenersTimeout = Timers.setTimeout(
+      this._onUpdateListeners, 200);
   },
 
   /**
    * Get the filter state for a specific toggle button.
    *
    * @param string aToggleType
    * @returns boolean
    */
   getFilterState: function WCF_getFilterState(aToggleType)
   {
     return this.filterPrefs[aToggleType];
   },
 
   /**
+   * Called when a logging filter changes. Allows to stop/start
+   * listeners according to the current filter state.
+   */
+  _onUpdateListeners: function() {
+    this._updateReflowActivityListener();
+    this._updateServerLoggingListener();
+  },
+
+  /**
    * Check that the passed string matches the filter arguments.
    *
    * @param String aString
    *        to search for filter words in.
    * @param String aFilter
    *        is a string containing all of the words to filter on.
    * @returns boolean
    */
@@ -4977,16 +5034,17 @@ function WebConsoleConnectionProxy(aWebC
 
   this._onPageError = this._onPageError.bind(this);
   this._onLogMessage = this._onLogMessage.bind(this);
   this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onFileActivity = this._onFileActivity.bind(this);
   this._onReflowActivity = this._onReflowActivity.bind(this);
+  this._onServerLogCall = this._onServerLogCall.bind(this);
   this._onTabNavigated = this._onTabNavigated.bind(this);
   this._onAttachConsole = this._onAttachConsole.bind(this);
   this._onCachedMessages = this._onCachedMessages.bind(this);
   this._connectionTimeout = this._connectionTimeout.bind(this);
   this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this);
 }
 
 WebConsoleConnectionProxy.prototype = {
@@ -5082,16 +5140,17 @@ WebConsoleConnectionProxy.prototype = {
 
     let client = this.client = this.target.client;
 
     client.addListener("logMessage", this._onLogMessage);
     client.addListener("pageError", this._onPageError);
     client.addListener("consoleAPICall", this._onConsoleAPICall);
     client.addListener("fileActivity", this._onFileActivity);
     client.addListener("reflowActivity", this._onReflowActivity);
+    client.addListener("serverLogCall", this._onServerLogCall);
     client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
     this.target.on("will-navigate", this._onTabNavigated);
     this.target.on("navigate", this._onTabNavigated);
 
     this._consoleActor = this.target.form.consoleActor;
     if (this.target.isTabActor) {
       let tab = this.target.form;
       this.owner.onLocationChange(tab.url, tab.title);
@@ -5150,17 +5209,17 @@ WebConsoleConnectionProxy.prototype = {
 
     this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
     this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
     this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
 
     let msgs = ["PageError", "ConsoleAPI"];
     this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
 
-    this.owner._updateReflowActivityListener();
+    this.owner._onUpdateListeners();
   },
 
   /**
    * The "cachedMessages" response handler.
    *
    * @private
    * @param object aResponse
    *        The JSON response object received from the server.
@@ -5300,16 +5359,33 @@ WebConsoleConnectionProxy.prototype = {
   _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
   {
     if (this.owner && aPacket.from == this._consoleActor) {
       this.owner.handleReflowActivity(aPacket);
     }
   },
 
   /**
+   * The "serverLogCall" message type handler. We redirect any message to
+   * the UI for displaying.
+   *
+   * @private
+   * @param string aType
+   *        Message type.
+   * @param object aPacket
+   *        The message received from the server.
+   */
+  _onServerLogCall: function WCCP__onServerLogCall(aType, aPacket)
+  {
+    if (this.owner && aPacket.from == this._consoleActor) {
+      this.owner.handleConsoleAPICall(aPacket.message);
+    }
+  },
+
+  /**
    * The "lastPrivateContextExited" message type handler. When this message is
    * received the Web Console UI is cleared.
    *
    * @private
    * @param string aType
    *        Message type.
    * @param object aPacket
    *        The message received from the server.
@@ -5373,16 +5449,17 @@ WebConsoleConnectionProxy.prototype = {
       return this._disconnecter.promise;
     }
 
     this.client.removeListener("logMessage", this._onLogMessage);
     this.client.removeListener("pageError", this._onPageError);
     this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
     this.client.removeListener("fileActivity", this._onFileActivity);
     this.client.removeListener("reflowActivity", this._onReflowActivity);
+    this.client.removeListener("serverLogCall", this._onServerLogCall);
     this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
     this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
     this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
     this.target.off("will-navigate", this._onTabNavigated);
     this.target.off("navigate", this._onTabNavigated);
 
     this.client = null;
     this.webConsoleClient = null;
@@ -5469,16 +5546,19 @@ ConsoleContextMenu.prototype = {
           selection.add("css");
           break;
         case CATEGORY_JS:
           selection.add("js");
           break;
         case CATEGORY_WEBDEV:
           selection.add("webdev");
           break;
+        case CATEGORY_SERVER:
+          selection.add("server");
+          break;
       }
     }
 
     return metadata;
   },
 
   /*
    * Determine if an item should be hidden.
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -167,21 +167,37 @@ function goUpdateConsoleCommands() {
               <menuitem label="&btnConsoleSharedWorkers;" type="checkbox"
                         autocheck="false" prefKey="sharedworkers"/>
               <menuitem label="&btnConsoleServiceWorkers;" type="checkbox"
                         autocheck="false" prefKey="serviceworkers"/>
               <menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox"
                         autocheck="false" prefKey="windowlessworkers"/>
             </menupopup>
           </toolbarbutton>
+          <toolbarbutton label="&btnServerLogging.label;" type="menu-button"
+                         category="server" class="devtools-toolbarbutton webconsole-filter-button"
+                         tooltiptext="&btnServerLogging.tooltip;"
+                         accesskey="&btnServerLogging.accesskey;"
+                         tabindex="8">
+            <menupopup id="server-logging-contextmenu">
+              <menuitem label="&btnServerErrors;" type="checkbox"
+                        autocheck="false" prefKey="servererror"/>
+              <menuitem label="&btnServerWarnings;" type="checkbox"
+                        autocheck="false" prefKey="serverwarn"/>
+              <menuitem label="&btnServerInfo;" type="checkbox" autocheck="false"
+                        prefKey="serverinfo"/>
+              <menuitem label="&btnServerLog;" type="checkbox" autocheck="false"
+                        prefKey="serverlog"/>
+            </menupopup>
+          </toolbarbutton>
         </hbox>
         <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
                        label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"
                        accesskey="&btnClear.accesskey;"
-                       tabindex="8"/>
+                       tabindex="9"/>
 
         <spacer flex="1"/>
 
         <textbox class="compact hud-filter-box devtools-searchinput" type="search"
                  placeholder="&filterOutput.placeholder;" tabindex="2"/>
       </toolbar>
 
       <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip">
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -71,16 +71,27 @@
 <!ENTITY btnPageLogging.accesskey3 "L">
 <!ENTITY btnConsoleErrors       "Errors">
 <!ENTITY btnConsoleInfo         "Info">
 <!ENTITY btnConsoleWarnings     "Warnings">
 <!ENTITY btnConsoleLog          "Log">
 <!ENTITY btnConsoleXhr          "XHR">
 <!ENTITY btnConsoleReflows      "Reflows">
 
+<!-- LOCALIZATION NOTE (btnServerLogging): This is used as the text of the
+  -  the toolbar. It shows or hides messages that the web developer inserted on
+  -  the page for debugging purposes, using calls on the HTTP server. -->
+<!ENTITY btnServerLogging.label       "Server">
+<!ENTITY btnServerLogging.tooltip     "Log messages received from a web server">
+<!ENTITY btnServerLogging.accesskey   "S">
+<!ENTITY btnServerErrors              "Errors">
+<!ENTITY btnServerInfo                "Info">
+<!ENTITY btnServerWarnings            "Warnings">
+<!ENTITY btnServerLog                 "Log">
+
 <!-- LOCALIZATION NODE (btnConsoleSharedWorkers) the term "Shared Workers"
   -  should not be translated. -->
 <!ENTITY btnConsoleSharedWorkers "Shared Workers">
 <!-- LOCALIZATION NODE (btnConsoleServiceWorkers) the term "Service Workers"
   -  should not be translated. -->
 <!ENTITY btnConsoleServiceWorkers "Service Workers">
 <!-- LOCALIZATION NODE (btnConsoleWindowlessWorkers) the term "Workers"
   -  should not be translated. -->
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -314,28 +314,42 @@ a {
   border-color: #929292;
 }
 
 .message[category=console] > .indent {
   -moz-border-end: solid #cbcbcb 6px;
 }
 
 .message[category=console][severity=error] > .icon::before,
-.message[category=output][severity=error] > .icon::before {
+.message[category=output][severity=error] > .icon::before,
+.message[category=server][severity=error] > .icon::before {
   background-position: -12px -36px;
 }
 
-.message[category=console][severity=warn] > .icon::before {
+.message[category=console][severity=warn] > .icon::before,
+.message[category=server][severity=warn] > .icon::before {
   background-position: -24px -36px;
 }
 
-.message[category=console][severity=info] > .icon::before {
+.message[category=console][severity=info] > .icon::before,
+.message[category=server][severity=info] > .icon::before {
   background-position: -36px -36px;
 }
 
+/* Server Logging Styles */
+
+.webconsole-filter-button[category="server"] > .toolbarbutton-menubutton-button:before {
+  background-image: linear-gradient(rgb(144, 176, 144), rgb(99, 151, 99));
+  border-color: rgb(76, 143, 76);
+}
+
+.message[category=server] > .indent {
+  -moz-border-end: solid #90B090 6px;
+}
+
 /* Input and output styles */
 .message[category=input] > .indent,
 .message[category=output] > .indent {
   -moz-border-end: solid #808080 6px;
 }
 
 .message[category=input] > .icon::before {
   background-position: -48px -36px;
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -26,16 +26,20 @@ XPCOMUtils.defineLazyGetter(this, "Netwo
 });
 XPCOMUtils.defineLazyGetter(this, "ConsoleProgressListener", () => {
   return require("devtools/toolkit/webconsole/network-monitor")
          .ConsoleProgressListener;
 });
 XPCOMUtils.defineLazyGetter(this, "events", () => {
   return require("sdk/event/core");
 });
+XPCOMUtils.defineLazyGetter(this, "ServerLoggingListener", () => {
+  return require("devtools/toolkit/webconsole/server-logger")
+         .ServerLoggingListener;
+});
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands", "JSPropertyProvider",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
   Object.defineProperty(this, name, {
     get: function(prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
@@ -349,24 +353,32 @@ WebConsoleActor.prototype =
     if (this.consoleProgressListener) {
       this.consoleProgressListener.destroy();
       this.consoleProgressListener = null;
     }
     if (this.consoleReflowListener) {
       this.consoleReflowListener.destroy();
       this.consoleReflowListener = null;
     }
-    events.off(this.parentActor, "changed-toplevel-document", this._onChangedToplevelDocument);
+    if (this.serverLoggingListener) {
+      this.serverLoggingListener.destroy();
+      this.serverLoggingListener = null;
+    }
+
+    events.off(this.parentActor, "changed-toplevel-document",
+               this._onChangedToplevelDocument);
+
     this.conn.removeActorPool(this._actorPool);
+
     if (this.parentActor.isRootActor) {
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
+
     this._actorPool = null;
-
     this._webConsoleCommandsCache = null;
     this._lastConsoleInputEvaluation = null;
     this._evalWindow = null;
     this._netEvents.clear();
     this.dbg.enabled = false;
     this.dbg = null;
     this.conn = null;
   },
@@ -599,16 +611,23 @@ WebConsoleActor.prototype =
           break;
         case "ReflowActivity":
           if (!this.consoleReflowListener) {
             this.consoleReflowListener =
               new ConsoleReflowListener(this.window, this);
           }
           startedListeners.push(listener);
           break;
+        case "ServerLogging":
+          if (!this.serverLoggingListener) {
+            this.serverLoggingListener =
+              new ServerLoggingListener(this.window, this);
+          }
+          startedListeners.push(listener);
+          break;
       }
     }
 
     // Update the live list of running listeners
     startedListeners.forEach(this._listeners.add, this._listeners);
 
     return {
       startedListeners: startedListeners,
@@ -629,17 +648,17 @@ WebConsoleActor.prototype =
   onStopListeners: function WCA_onStopListeners(aRequest)
   {
     let stoppedListeners = [];
 
     // If no specific listeners are requested to be detached, we stop all
     // listeners.
     let toDetach = aRequest.listeners ||
                    ["PageError", "ConsoleAPI", "NetworkActivity",
-                    "FileActivity"];
+                    "FileActivity", "ServerLogging"];
 
     while (toDetach.length > 0) {
       let listener = toDetach.shift();
       switch (listener) {
         case "PageError":
           if (this.consoleServiceListener) {
             this.consoleServiceListener.destroy();
             this.consoleServiceListener = null;
@@ -670,16 +689,23 @@ WebConsoleActor.prototype =
           break;
         case "ReflowActivity":
           if (this.consoleReflowListener) {
             this.consoleReflowListener.destroy();
             this.consoleReflowListener = null;
           }
           stoppedListeners.push(listener);
           break;
+        case "ServerLogging":
+          if (this.serverLoggingListener) {
+            this.serverLoggingListener.destroy();
+            this.serverLoggingListener = null;
+          }
+          stoppedListeners.push(listener);
+          break;
       }
     }
 
     // Update the live list of running listeners
     stoppedListeners.forEach(this._listeners.delete, this._listeners);
 
     return { stoppedListeners: stoppedListeners };
   },
@@ -1422,51 +1448,90 @@ WebConsoleActor.prototype =
       sourceURL: aReflowInfo.sourceURL,
       sourceLine: aReflowInfo.sourceLine,
       functionName: aReflowInfo.functionName
     };
 
     this.conn.send(packet);
   },
 
+  /**
+   * Handler for server logging. This method forwards log events to the
+   * remote Web Console client.
+   *
+   * @see ServerLoggingListener
+   * @param object aMessage
+   *        The console API call on the server we need to send to the remote client.
+   */
+  onServerLogCall: function WCA_onServerLogCall(aMessage)
+  {
+    // Clone all data into the content scope (that's where
+    // passed arguments comes from).
+    let msg = Cu.cloneInto(aMessage, this.window);
+
+    // All arguments within the message need to be converted into
+    // debuggees to properly send it to the client side.
+    // Use the default target: this.window as the global object
+    // since that's the correct scope for data in the message.
+    // The 'false' argument passed into prepareConsoleMessageForRemote()
+    // ensures that makeDebuggeeValue uses content debuggee.
+    // See also:
+    // * makeDebuggeeValue()
+    // * prepareConsoleMessageForRemote()
+    msg = this.prepareConsoleMessageForRemote(msg, false);
+
+    let packet = {
+      from: this.actorID,
+      type: "serverLogCall",
+      message: msg,
+    };
+
+    this.conn.send(packet);
+  },
+
   //////////////////
   // End of event handlers for various listeners.
   //////////////////
 
   /**
    * Prepare a message from the console API to be sent to the remote Web Console
    * instance.
    *
    * @param object aMessage
    *        The original message received from console-api-log-event.
+   * @param boolean aUseObjectGlobal
+   *        If |true| the object global is determined and added as a debuggee,
+   *        otherwise |this.window| is used when makeDebuggeeValue() is invoked.
    * @return object
    *         The object that can be sent to the remote client.
    */
   prepareConsoleMessageForRemote:
-  function WCA_prepareConsoleMessageForRemote(aMessage)
+  function WCA_prepareConsoleMessageForRemote(aMessage, aUseObjectGlobal = true)
   {
     let result = WebConsoleUtils.cloneObject(aMessage);
 
     result.workerType = CONSOLE_WORKER_IDS.indexOf(result.innerID) == -1
                           ? 'none' : result.innerID;
 
     delete result.wrappedJSObject;
     delete result.ID;
     delete result.innerID;
     delete result.consoleID;
 
     result.arguments = Array.map(aMessage.arguments || [], (aObj) => {
-      let dbgObj = this.makeDebuggeeValue(aObj, true);
+      let dbgObj = this.makeDebuggeeValue(aObj, aUseObjectGlobal);
       return this.createValueGrip(dbgObj);
     });
 
     result.styles = Array.map(aMessage.styles || [], (aString) => {
       return this.createValueGrip(aString);
     });
 
+    result.category = aMessage.category || "webdev";
+
     return result;
   },
 
   /**
    * Find the XUL window that owns the content window.
    *
    * @return Window
    *         The XUL window that owns the content window.
--- a/toolkit/devtools/webconsole/moz.build
+++ b/toolkit/devtools/webconsole/moz.build
@@ -7,10 +7,12 @@
 if CONFIG['OS_TARGET'] != 'Android':
     MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
     XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 EXTRA_JS_MODULES.devtools.toolkit.webconsole += [
     'client.js',
     'network-helper.js',
     'network-monitor.js',
+    'server-logger-monitor.js',
+    'server-logger.js',
     'utils.js',
 ]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/server-logger-monitor.js
@@ -0,0 +1,211 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+const {Ci} = require("chrome");
+const Services = require("Services");
+
+const {DebuggerServer} = require("devtools/server/main");
+const {makeInfallible} = require("devtools/toolkit/DevToolsUtils");
+
+loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+  log: function(...args) {
+  }
+}
+
+const acceptableHeaders = ["x-chromelogger-data"];
+
+/**
+ * This object represents HTTP events observer. It's intended to be
+ * used in e10s enabled browser only.
+ *
+ * Since child processes can't register HTTP event observer they use
+ * this module to do the observing in the parent process. This monitor
+ * is loaded through DebuggerServerConnection.setupInParent() that is executed
+ * from within the child process. The execution is done by {@ServerLoggingListener}.
+ * The monitor listens to HTTP events and forwards it into the right child process.
+ *
+ * Read more about the architecture:
+ * https://github.com/mozilla/gecko-dev/blob/fx-team/toolkit/devtools/server/docs/actor-e10s-handling.md
+ */
+var ServerLoggerMonitor = {
+  // Initialization
+
+  initialize: function() {
+    this.onChildMessage = this.onChildMessage.bind(this);
+    this.onDisconnectChild = this.onDisconnectChild.bind(this);
+    this.onExamineResponse = this.onExamineResponse.bind(this);
+
+    // Set of tracked message managers.
+    this.messageManagers = new Set();
+
+    // Set of registered child frames (loggers).
+    this.targets = new Set();
+  },
+
+  // Parent Child Relationship
+
+  attach: makeInfallible(function({mm, prefix}) {
+    let size = this.messageManagers.size;
+
+    trace.log("ServerLoggerMonitor.attach; ", size, arguments);
+
+    if (this.messageManagers.has(mm)) {
+      return;
+    }
+
+    this.messageManagers.add(mm);
+
+    // Start listening for messages from the {@ServerLogger} actor
+    // living in the child process.
+    mm.addMessageListener("debug:server-logger", this.onChildMessage);
+
+    // Listen to the disconnection message to clean-up.
+    DebuggerServer.once("disconnected-from-child:" + prefix,
+      this.onDisconnectChild);
+  }),
+
+  detach: function(mm) {
+    let size = this.messageManagers.size;
+
+    trace.log("ServerLoggerMonitor.detach; ", size);
+
+    // Unregister message listeners
+    mm.removeMessageListener("debug:server-logger", this.onChildMessage);
+  },
+
+  onDisconnectChild: function(event, mm) {
+    let size = this.messageManagers.size;
+
+    trace.log("ServerLoggerMonitor.onDisconnectChild; ",
+      size, arguments);
+
+    if (!this.messageManagers.has(mm)) {
+      return;
+    }
+
+    this.detach(mm);
+
+    this.messageManagers.delete(mm);
+  },
+
+  // Child Message Handling
+
+  onChildMessage: function(msg) {
+    let method = msg.data.method;
+
+    trace.log("ServerLoggerMonitor.onChildMessage; ", method, msg);
+
+    switch (method) {
+      case "attachChild":
+        return this.onAttachChild(msg);
+      case "detachChild":
+        return this.onDetachChild(msg);
+      default:
+        trace.log("Unknown method name: ", method);
+    }
+  },
+
+  onAttachChild: function(event) {
+    let target = event.target;
+    let size = this.targets.size;
+
+    trace.log("ServerLoggerMonitor.onAttachChild; size: ", size, target);
+
+    // If this is the first child attached, register global HTTP observer.
+    if (!size) {
+      trace.log("ServerLoggerMonitor.onAttatchChild; Add HTTP Observer");
+      Services.obs.addObserver(this.onExamineResponse,
+        "http-on-examine-response", false);
+    }
+
+    // Collect child loggers. The frame element where the
+    // window/document lives.
+    this.targets.add(target);
+  },
+
+  onDetachChild: function(event) {
+    let target = event.target;
+    this.targets.delete(target);
+
+    let size = this.targets.size;
+    trace.log("ServerLoggerMonitor.onDetachChild; size: ", size, target);
+
+    // If this is the last child process attached, unregister
+    // the global HTTP observer.
+    if (!size) {
+      trace.log("ServerLoggerMonitor.onDetachChild; Remove HTTP Observer");
+      Services.obs.removeObserver(this.onExamineResponse,
+        "http-on-examine-response", false);
+    }
+  },
+
+  // HTTP Observer
+
+  onExamineResponse: makeInfallible(function(subject, topic) {
+    let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+    trace.log("ServerLoggerMonitor.onExamineResponse; ", httpChannel.name,
+      this.targets);
+
+    // Ignore requests from chrome or add-on code when we are monitoring
+    // content.
+    if (!httpChannel.loadInfo &&
+        httpChannel.loadInfo.loadingDocument === null &&
+        httpChannel.loadInfo.loadingPrincipal === Services.scriptSecurityManager.getSystemPrincipal()) {
+      return;
+    }
+
+    let requestFrame = NetworkHelper.getTopFrameForRequest(httpChannel);
+    if (!requestFrame) {
+      return;
+    }
+
+    // Ignore requests from parent frames that aren't registered.
+    if (!this.targets.has(requestFrame)) {
+      return;
+    }
+
+    let headers = [];
+
+    httpChannel.visitResponseHeaders((header, value) => {
+      header = header.toLowerCase();
+      if (acceptableHeaders.indexOf(header) !== -1) {
+        headers.push({header: header, value: value});
+      }
+    });
+
+    if (!headers.length) {
+      return;
+    }
+
+    let { messageManager } = requestFrame;
+    messageManager.sendAsyncMessage("debug:server-logger", {
+      method: "examineHeaders",
+      headers: headers,
+    });
+
+    trace.log("ServerLoggerMonitor.onExamineResponse; headers ",
+      headers.length, ", ", headers);
+  }),
+};
+
+/**
+ * Executed automatically by the framework.
+ */
+function setupParentProcess(event) {
+  ServerLoggerMonitor.attach(event);
+}
+
+// Monitor initialization.
+ServerLoggerMonitor.initialize();
+
+// Exports from this module
+exports.setupParentProcess = setupParentProcess;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/server-logger.js
@@ -0,0 +1,531 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+const {Cu, Ci} = require("chrome");
+const {Class} = require("sdk/core/heritage");
+const Services = require("Services");
+
+const {DebuggerServer} = require("devtools/server/main");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+
+Cu.importGlobalProperties(["atob"]);
+
+loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+  log: function(...args) {
+  }
+}
+
+// Constants
+const makeInfallible = DevToolsUtils.makeInfallible;
+const acceptableHeaders = ["x-chromelogger-data"];
+
+/**
+ * The listener is responsible for detecting server side logs
+ * within HTTP headers and sending them to the client.
+ *
+ * The logic is based on "http-on-examine-response" event that is
+ * sent when a response from the server is received. Consequently HTTP
+ * headers are parsed to find server side logs.
+ *
+ * A listeners for "http-on-examine-response" is registered when
+ * the listener starts and removed when destroy is executed.
+ */
+var ServerLoggingListener = Class({
+  /**
+   * Initialization of the listener. The main step during the initialization
+   * process is registering a listener for "http-on-examine-response" event.
+   *
+   * @param {Object} win (nsIDOMWindow):
+   *        filter network requests by the associated window object.
+   *        If null (i.e. in the browser context) log everything
+   * @param {Object} owner
+   *        The {@WebConsoleActor} instance
+   */
+  initialize: function(win, owner) {
+    trace.log("ServerLoggingListener.initialize; ", owner.actorID,
+      ", child process: ", DebuggerServer.isInChildProcess);
+
+    this.owner = owner;
+    this.window = win;
+
+    this.onExamineResponse = this.onExamineResponse.bind(this);
+    this.onExamineHeaders = this.onExamineHeaders.bind(this);
+    this.onParentMessage = this.onParentMessage.bind(this);
+
+    this.attach();
+  },
+
+  /**
+   * The destroy is called by the parent WebConsoleActor actor.
+   */
+  destroy: function() {
+    trace.log("ServerLoggingListener.destroy; ", this.owner.actorID,
+      ", child process: ", DebuggerServer.isInChildProcess);
+
+    this.detach();
+  },
+
+  /**
+   * The main responsibility of this method is registering a listener for
+   * "http-on-examine-response" events.
+   */
+  attach: makeInfallible(function() {
+    trace.log("ServerLoggingListener.attach; child process: ",
+      DebuggerServer.isInChildProcess);
+
+    // Setup the child <-> parent communication if this actor module
+    // is running in a child process. If e10s is disabled (this actor
+    // running in the same process as everything else) register observer
+    // listener just like in good old pre e10s days.
+    if (DebuggerServer.isInChildProcess) {
+      this.attachParentProcess();
+    } else {
+      Services.obs.addObserver(this.onExamineResponse,
+        "http-on-examine-response", false);
+    }
+  }),
+
+  /**
+   * Remove the "http-on-examine-response" listener.
+   */
+  detach: makeInfallible(function() {
+    trace.log("ServerLoggingListener.detach; ", this.owner.actorID);
+
+    if (DebuggerServer.isInChildProcess) {
+      this.detachParentProcess();
+    } else {
+      Services.obs.removeObserver(this.onExamineResponse,
+        "http-on-examine-response", false);
+    }
+  }),
+
+  // Parent Child Relationship
+
+  attachParentProcess: function() {
+    trace.log("ServerLoggingListener.attachParentProcess;");
+
+    this.owner.conn.setupInParent({
+      module: "devtools/toolkit/webconsole/server-logger-monitor",
+      setupParent: "setupParentProcess"
+    });
+
+    let mm = this.owner.conn.parentMessageManager;
+    let { addMessageListener, sendSyncMessage } = mm;
+
+    // It isn't possible to register HTTP-* event observer inside
+    // a child process (in case of e10s), so listen for messages
+    // coming from the {@ServerLoggerMonitor} that lives inside
+    // the parent process.
+    addMessageListener("debug:server-logger", this.onParentMessage);
+
+    // Attach to the {@ServerLoggerMonitor} object to subscribe events.
+    sendSyncMessage("debug:server-logger", {
+      method: "attachChild"
+    });
+  },
+
+  detachParentProcess: makeInfallible(function() {
+    trace.log("ServerLoggingListener.detachParentProcess;");
+
+    let mm = this.owner.conn.parentMessageManager;
+    let { removeMessageListener, sendSyncMessage } = mm;
+
+    sendSyncMessage("debug:server-logger", {
+      method: "detachChild",
+    });
+
+    removeMessageListener("debug:server-logger", this.onParentMessage);
+  }),
+
+  onParentMessage: makeInfallible(function(msg) {
+    if (!msg.data) {
+      return;
+    }
+
+    let method = msg.data.method;
+    trace.log("ServerLogger.onParentMessage; ", method, msg.data);
+
+    switch (method) {
+      case "examineHeaders":
+        return this.onExamineHeaders(msg);
+      default:
+        trace.log("Unknown method name: ", method);
+    }
+  }),
+
+  // HTTP Observer
+
+  onExamineHeaders: function(event) {
+    let headers = event.data.headers;
+
+    trace.log("ServerLoggingListener.onExamineHeaders;", headers);
+
+    let parsedMessages = [];
+
+    for (let item of headers) {
+      let header = item.header;
+      let value = item.value;
+
+      let messages = this.parse(header, value);
+      if (messages) {
+        parsedMessages.push(...messages);
+      }
+    }
+
+    if (!parsedMessages.length) {
+      return;
+    }
+
+    for (let message of parsedMessages) {
+      this.sendMessage(message);
+    }
+  },
+
+  onExamineResponse: makeInfallible(function(subject) {
+    let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+    trace.log("ServerLoggingListener.onExamineResponse; ", httpChannel.name,
+      ", ", this.owner.actorID, httpChannel);
+
+    if (!this._matchRequest(httpChannel)) {
+      trace.log("ServerLoggerMonitor.onExamineResponse; No matching request!");
+      return;
+    }
+
+    let headers = [];
+
+    httpChannel.visitResponseHeaders((header, value) => {
+      header = header.toLowerCase();
+      if (acceptableHeaders.indexOf(header) !== -1) {
+        headers.push({header: header, value: value});
+      }
+    });
+
+    this.onExamineHeaders({
+      data: {
+        headers: headers,
+      }
+    });
+  }),
+
+  /**
+   * Check if a given network request should be logged by this network monitor
+   * instance based on the current filters.
+   *
+   * @private
+   * @param nsIHttpChannel aChannel
+   *        Request to check.
+   * @return boolean
+   *         True if the network request should be logged, false otherwise.
+   */
+  _matchRequest: function(aChannel) {
+    trace.log("_matchRequest ", this.window, ", ", this.topFrame);
+
+    // Log everything if the window is null (it's null in the browser context)
+    if (!this.window) {
+      return true;
+    }
+
+    // Ignore requests from chrome or add-on code when we are monitoring
+    // content.
+    if (!aChannel.loadInfo &&
+        aChannel.loadInfo.loadingDocument === null &&
+        aChannel.loadInfo.loadingPrincipal === Services.scriptSecurityManager.getSystemPrincipal()) {
+      return false;
+    }
+
+    // Since frames support, this.window may not be the top level content
+    // frame, so that we can't only compare with win.top.
+    let win = NetworkHelper.getWindowForRequest(aChannel);
+    while(win) {
+      if (win == this.window) {
+        return true;
+      }
+      if (win.parent == win) {
+        break;
+      }
+      win = win.parent;
+    }
+
+    return false;
+  },
+
+  // Server Logs
+
+  /**
+   * Search through HTTP headers to catch all server side logs.
+   * Learn more about the data structure:
+   * https://craig.is/writing/chrome-logger/techspecs
+   */
+  parse: function(header, value) {
+    let data;
+
+    try {
+      let result = decodeURIComponent(escape(atob(value)));
+      data = JSON.parse(result);
+    } catch (err) {
+      Cu.reportError("Failed to parse HTTP log data! " + err);
+      return;
+    }
+
+    let parsedMessage = [];
+    let columnMap = this.getColumnMap(data);
+
+    trace.log("ServerLoggingListener.parse; ColumnMap", columnMap);
+    trace.log("ServerLoggingListener.parse; data", data);
+
+    let lastLocation;
+
+    for (let row of data.rows) {
+      let backtrace = row[columnMap.get("backtrace")];
+      let rawLogs = row[columnMap.get("log")];
+      let type = row[columnMap.get("type")] || "log";
+
+      // Old version of the protocol includes a label.
+      // If this is the old version do some converting.
+      if (data.columns.indexOf("label") != -1) {
+        let label = row[columnMap.get("label")];
+        let showLabel = label && typeof label === "string";
+
+        rawLogs = [rawLogs];
+
+        if (showLabel) {
+          rawLogs.unshift(label);
+        }
+      }
+
+      // If multiple logs come from the same line only the first log
+      // has info about the backtrace. So, remember the last valid
+      // location and use it for those that not set.
+      let location = this.parseBacktrace(backtrace);
+      if (location) {
+        lastLocation = location;
+      } else {
+        location = lastLocation;
+      }
+
+      parsedMessage.push({
+        logs: rawLogs,
+        location: location,
+        type: type
+      });
+    }
+
+    return parsedMessage;
+  },
+
+  parseBacktrace: function(backtrace) {
+    if (!backtrace) {
+      return null;
+    }
+
+    let result = backtrace.match(/\s*(\d+)$/);
+    if (!result || result.length < 2) {
+      return backtrace;
+    }
+
+    return {
+      url: backtrace.slice(0, -result[0].length),
+      line: result[1]
+    };
+  },
+
+  getColumnMap: function(data) {
+    let columnMap = new Map();
+    let columnName;
+
+    for (let key in data.columns) {
+      columnName = data.columns[key];
+      columnMap.set(columnName, key);
+    }
+
+    return columnMap;
+  },
+
+  sendMessage: function(msg) {
+    trace.log("ServerLoggingListener.sendMessage; message", msg);
+
+    let formatted = format(msg);
+    trace.log("ServerLoggingListener.sendMessage; formatted", formatted);
+
+    let win = this.window;
+    let innerID = win ? getInnerId(win) : null;
+    let location = msg.location;
+
+    let message = {
+      category: "server",
+      innerID: innerID,
+      level: msg.type,
+      filename: location ? location.url : null,
+      lineNumber: location ? location.line : null,
+      columnNumber: 0,
+      private: false,
+      timeStamp: Date.now(),
+      arguments: formatted ? formatted.logs : null,
+      styles: formatted ? formatted.styles : null,
+    };
+
+    // Make sure to set the group name.
+    if (msg.type == "group" && formatted && formatted.logs) {
+      message.groupName = formatted ? formatted.logs[0] : null;
+    }
+
+    // A message for console.table() method (passed in as the first
+    // argument) isn't supported. But, it's passed in by some server
+    // side libraries that implement console.* API - let's just remove it.
+    let args = message.arguments;
+    if (msg.type == "table" && args) {
+      if (typeof args[0] == "string") {
+        args.shift();
+      }
+    }
+
+    trace.log("ServerLoggingListener.sendMessage; raw: ",
+      msg.logs.join(", "), message);
+
+    this.owner.onServerLogCall(message);
+  },
+});
+
+// Helpers
+
+/**
+ * Parse printf-like specifiers ("%f", "%d", ...) and
+ * format the logs according to them.
+ */
+function format(msg) {
+  if (!msg.logs || !msg.logs[0]) {
+    return;
+  }
+
+  // Initialize the styles array (used for the "%c" specifier).
+  msg.styles = [];
+
+  // Remove and get the first log (in which the specifiers are).
+  let firstString = msg.logs.shift();
+  // Contains all the strings split by the specifiers
+  // (i.e. "a %f b" => ["a ", " b"]).
+  let splitLog = [];
+  // All the specifiers present in the first string.
+  let specifiers = [];
+  let specifierIndex = -1;
+  let splitLogRegExp = /(.*?)(%[oOcsdif]|$)/g;
+  let splitLogRegExpRes;
+
+  // Get the strings before the specifiers (or the last chunk before the end
+  // of the string).
+  while ((splitLogRegExpRes = splitLogRegExp.exec(firstString)) !== null) {
+    let [_, log, specifier] = splitLogRegExpRes;
+
+    // We can add an empty string if there is a specifier after (which
+    // means we haven't reached the end of the string). This empty string is
+    // necessary when rebuilding the logs after the formatting (we should ensure
+    // to alternate a log + a specifier to replace to make this loop work).
+    //
+    // Example: "%ctest" => first iteration: log = "", specifier = "%c".
+    //                   => second iteration: log = "test", specifier = "".
+    if (log || specifier) {
+      splitLog.push(log);
+    }
+
+    // Break now if there is no specifier anymore
+    // (means that we have reached the end of the string).
+    if (!specifier) {
+      break;
+    }
+
+    specifiers.push(specifier);
+  }
+
+  // This array represents the string of the log, in which the specifiers
+  // are replaced. It alternates strings and objects (%o;%O).
+  let rebuiltLogArray = [];
+  let concatString = "";
+  let pushConcatString = () => {
+    if (concatString) {
+      rebuiltLogArray.push(concatString);
+    }
+    concatString = "";
+  };
+
+  // Merge the split first string and the values associated to the specifiers.
+  splitLog.forEach((string, index) => {
+    // Concatenate the string in any case.
+    concatString += string;
+    if (specifiers.length === 0) {
+      return;
+    }
+
+    let argument = msg.logs.shift();
+    switch (specifiers[index]) {
+      case "%i":
+      case "%d":
+        // Parse into integer.
+        argument |= 0;
+        concatString += argument;
+        break;
+      case "%f":
+        // Parse into float.
+        argument =+ argument;
+        concatString += argument;
+        break;
+      case "%o":
+      case "%O":
+        // Push the concatenated string and reinitialize concatString.
+        pushConcatString();
+        // Push the object.
+        rebuiltLogArray.push(argument);
+        break;
+      case "%s":
+        concatString += argument;
+        break;
+      case "%c":
+        pushConcatString();
+        for (let j = msg.styles.length; j < rebuiltLogArray.length; j++) {
+          msg.styles.push(null);
+        }
+        msg.styles.push(argument);
+        break;
+      default:
+        // Should never happen.
+        return;
+    }
+  });
+
+  if (concatString) {
+    rebuiltLogArray.push(concatString);
+  }
+
+  // Append the rest of arguments that don't have corresponding
+  // specifiers to the message logs.
+  msg.logs = rebuiltLogArray.concat(msg.logs);
+
+  // Remove special ___class_name property that isn't supported
+  // by the current implementation. This property represents object class
+  // allowing custom rendering in the console panel.
+  for (let log of msg.logs) {
+    if (typeof log == "object") {
+      delete log.___class_name;
+    }
+  }
+
+  return msg;
+}
+
+// These helper are cloned from SDK to avoid loading to
+// much SDK modules just because of two functions.
+function getInnerId(win) {
+  return win.QueryInterface(Ci.nsIInterfaceRequestor).
+    getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+};
+
+// Exports from this module
+exports.ServerLoggingListener = ServerLoggingListener;