diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 9e31d6e3bda8..953a24689ce0 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1562,6 +1562,10 @@ 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);
@@ -1583,6 +1587,10 @@ 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);
diff --git a/browser/devtools/webconsole/console-output.js b/browser/devtools/webconsole/console-output.js
index b3803a9b14f0..1180f770a8f8 100644
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -44,6 +44,7 @@ const COMPAT = {
INPUT: 4,
OUTPUT: 5,
SECURITY: 6,
+ SERVER: 7,
},
// The possible message severities.
@@ -68,11 +69,12 @@ const COMPAT = {
[ 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" ],
@@ -1300,7 +1302,7 @@ 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,
@@ -1571,7 +1573,7 @@ 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,
@@ -1708,7 +1710,7 @@ 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,
diff --git a/browser/devtools/webconsole/test/browser.ini b/browser/devtools/webconsole/test/browser.ini
index 4e5c4dda1655..a37e2ac3458e 100644
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -73,6 +73,7 @@ support-files =
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
@@ -172,6 +173,7 @@ skip-if = buildapp == 'mulet'
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]
diff --git a/browser/devtools/webconsole/test/browser_console_server_logging.js b/browser/devtools/webconsole/test/browser_console_server_logging.js
new file mode 100644
index 000000000000..715a9baf38ce
--- /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,
+ }],
+ })
+});
diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
index 09423bb22e36..cdc07114f6e8 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -27,12 +27,14 @@ function testFilterButtons() {
testMenuFilterButton("js");
testMenuFilterButton("logging");
testMenuFilterButton("security");
+ testMenuFilterButton("server");
testIsolateFilterButton("net");
testIsolateFilterButton("css");
testIsolateFilterButton("js");
testIsolateFilterButton("logging");
testIsolateFilterButton("security");
+ testIsolateFilterButton("server");
}
function testMenuFilterButton(category) {
diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js
index f5d64f829ef6..015cd7f3737c 100644
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -28,6 +28,7 @@ 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;
diff --git a/browser/devtools/webconsole/test/test-console-server-logging.sjs b/browser/devtools/webconsole/test/test-console-server-logging.sjs
new file mode 100644
index 000000000000..167a6e39a311
--- /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 = "" +
+ "
" +
+ "hello world!
" +
+ "";
+
+ 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
diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js
index 6d058292fe59..6f8de55a9591 100644
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -34,6 +34,7 @@ loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/Variabl
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);
@@ -76,6 +77,7 @@ 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.
@@ -93,6 +95,7 @@ const CATEGORY_CLASS_FRAGMENTS = [
"input",
"output",
"security",
+ "server",
];
// The fragment of a CSS class name that identifies each severity.
@@ -109,14 +112,15 @@ const SEVERITY_CLASS_FRAGMENTS = [
// 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
@@ -217,6 +221,7 @@ function WebConsoleFrame(aWebConsoleOwner)
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;
@@ -652,10 +657,12 @@ WebConsoleFrame.prototype = {
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);
}
},
@@ -666,7 +673,6 @@ WebConsoleFrame.prototype = {
* @param function [aCallback=null]
* Optional function to invoke when the listener has been
* added/removed.
- *
*/
_updateReflowActivityListener:
function WCF__updateReflowActivityListener(aCallback)
@@ -681,6 +687,38 @@ WebConsoleFrame.prototype = {
}
},
+ /**
+ * 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
@@ -753,6 +791,9 @@ WebConsoleFrame.prototype = {
let logging = this.document.querySelector("toolbarbutton[category=logging]");
logging.removeAttribute("accesskey");
+
+ let serverLogging = this.document.querySelector("toolbarbutton[category=server]");
+ serverLogging.removeAttribute("accesskey");
}
},
@@ -970,8 +1011,15 @@ WebConsoleFrame.prototype = {
{
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);
},
/**
@@ -985,6 +1033,15 @@ WebConsoleFrame.prototype = {
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.
*
@@ -4982,6 +5039,7 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget)
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);
@@ -5087,6 +5145,7 @@ WebConsoleConnectionProxy.prototype = {
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);
@@ -5155,7 +5214,7 @@ WebConsoleConnectionProxy.prototype = {
let msgs = ["PageError", "ConsoleAPI"];
this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
- this.owner._updateReflowActivityListener();
+ this.owner._onUpdateListeners();
},
/**
@@ -5304,6 +5363,23 @@ WebConsoleConnectionProxy.prototype = {
}
},
+ /**
+ * 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.
@@ -5378,6 +5454,7 @@ WebConsoleConnectionProxy.prototype = {
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);
@@ -5474,6 +5551,9 @@ ConsoleContextMenu.prototype = {
case CATEGORY_WEBDEV:
selection.add("webdev");
break;
+ case CATEGORY_SERVER:
+ selection.add("server");
+ break;
}
}
diff --git a/browser/devtools/webconsole/webconsole.xul b/browser/devtools/webconsole/webconsole.xul
index 79f1c96e9bc3..cd255e8ecfde 100644
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -172,11 +172,27 @@ function goUpdateConsoleCommands() {
autocheck="false" prefKey="windowlessworkers"/>
+
+
+
+ tabindex="9"/>
diff --git a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
index b6469146dc9c..4e60b892a5f5 100644
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -76,6 +76,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/browser/themes/shared/devtools/webconsole.inc.css b/browser/themes/shared/devtools/webconsole.inc.css
index a066d6264927..beb13ab1e16e 100644
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -319,18 +319,32 @@ a {
}
.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 {
diff --git a/toolkit/devtools/server/actors/webconsole.js b/toolkit/devtools/server/actors/webconsole.js
index fd48484386a2..ce9d10e12dda 100644
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -31,6 +31,10 @@ XPCOMUtils.defineLazyGetter(this, "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",
@@ -354,14 +358,22 @@ WebConsoleActor.prototype =
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._actorPool = null;
this._webConsoleCommandsCache = null;
this._lastConsoleInputEvaluation = null;
this._evalWindow = null;
@@ -604,6 +616,13 @@ WebConsoleActor.prototype =
}
startedListeners.push(listener);
break;
+ case "ServerLogging":
+ if (!this.serverLoggingListener) {
+ this.serverLoggingListener =
+ new ServerLoggingListener(this.window, this);
+ }
+ startedListeners.push(listener);
+ break;
}
}
@@ -634,7 +653,7 @@ WebConsoleActor.prototype =
// listeners.
let toDetach = aRequest.listeners ||
["PageError", "ConsoleAPI", "NetworkActivity",
- "FileActivity"];
+ "FileActivity", "ServerLogging"];
while (toDetach.length > 0) {
let listener = toDetach.shift();
@@ -675,6 +694,13 @@ WebConsoleActor.prototype =
}
stoppedListeners.push(listener);
break;
+ case "ServerLogging":
+ if (this.serverLoggingListener) {
+ this.serverLoggingListener.destroy();
+ this.serverLoggingListener = null;
+ }
+ stoppedListeners.push(listener);
+ break;
}
}
@@ -1427,6 +1453,40 @@ WebConsoleActor.prototype =
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.
//////////////////
@@ -1437,11 +1497,14 @@ WebConsoleActor.prototype =
*
* @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);
@@ -1454,7 +1517,7 @@ WebConsoleActor.prototype =
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);
});
@@ -1462,6 +1525,8 @@ WebConsoleActor.prototype =
return this.createValueGrip(aString);
});
+ result.category = aMessage.category || "webdev";
+
return result;
},
diff --git a/toolkit/devtools/webconsole/moz.build b/toolkit/devtools/webconsole/moz.build
index 621a69399b25..e9f493d615a2 100644
--- a/toolkit/devtools/webconsole/moz.build
+++ b/toolkit/devtools/webconsole/moz.build
@@ -12,5 +12,7 @@ EXTRA_JS_MODULES.devtools.toolkit.webconsole += [
'client.js',
'network-helper.js',
'network-monitor.js',
+ 'server-logger-monitor.js',
+ 'server-logger.js',
'utils.js',
]
diff --git a/toolkit/devtools/webconsole/server-logger-monitor.js b/toolkit/devtools/webconsole/server-logger-monitor.js
new file mode 100644
index 000000000000..4134fb7e18b3
--- /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;
diff --git a/toolkit/devtools/webconsole/server-logger.js b/toolkit/devtools/webconsole/server-logger.js
new file mode 100644
index 000000000000..edf2def8b3f5
--- /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;