Console.sys.mjs - mozsearch (original) (raw)

/* 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

* Define a 'console' API to roughly match the implementation provided by

* This module helps cases where code is shared between the web and Firefox.

* See also Browser.sys.mjs for an implementation of other web constants to help

* sharing code between the web and firefox;

* The API is only be a rough approximation for 3 reasons:

* - The Firebug console API is implemented in many places with differences in

* the implementations, so there isn't a single reference to adhere to

* - The Firebug console is a rich display compared with dump(), so there will

* be many things that we can't replicate

* - The primary use of this API is debugging and error logging so the perfect

* implementation isn't always required (or even well defined)

var gTimerRegistry = new Map();

* String utility to ensure that strings are a specified length. Strings

* that are too long are truncated to the max length and the last char is

* set to "_". Strings that are too short are padded with spaces.

* The string to format to the correct length

* @param {number} aMaxLen

* The maximum allowed length of the returned string

* @param {number} aMinLen (optional)

* The minimum allowed length of the returned string. If undefined,

* then aMaxLen will be used

* @param {object} aOptions (optional)

* An object allowing format customization. Allowed customizations:

* 'truncate' - can take the value "start" to truncate strings from

* the start as opposed to the end or "center" to truncate

* 'align' - takes an alignment when padding is needed for MinLen,

* either "start" or "end". Defaults to "start".

* The original string formatted to fit the specified lengths

function fmt(aStr, aMaxLen, aMinLen, aOptions) {

if (aStr.length > aMaxLen) {

if (aOptions && aOptions.truncate == "start") {

return "_" + aStr.substring(aStr.length - aMaxLen + 1);

} else if (aOptions && aOptions.truncate == "center") {

let start = aStr.substring(0, aMaxLen / 2);

let end = aStr.substring(aStr.length - aMaxLen / 2 + 1);

return start + "_" + end;

return aStr.substring(0, aMaxLen - 1) + "_";

if (aStr.length < aMinLen) {

let padding = Array(aMinLen - aStr.length + 1).join(" ");

aStr = aOptions.align === "end" ? padding + aStr : aStr + padding;

* Utility to extract the constructor name of an object.

* Object.toString gives: "[object ?????]"; we want the "?????".

* The object from which to extract the constructor name

function getCtorName(aObj) {

if (aObj === undefined) {

if (aObj.constructor && aObj.constructor.name) {

return aObj.constructor.name;

// If that fails, use Objects toString which sometimes gives something

// better than 'Object', and at least defaults to Object if nothing better

return Object.prototype.toString.call(aObj).slice(8, -1);

* Indicates whether an object is a JS or `Components.Exception` error.

function isError(aThing) {

((typeof aThing.name == "string" && aThing.name.startsWith("NS_ERROR_")) ||

getCtorName(aThing).endsWith("Error"))

* A single line stringification of an object designed for use by humans

* The object to be stringified

* @param {boolean} aAllowNewLines

* A single line representation of aThing, which will generally be at

function stringify(aThing, aAllowNewLines) {

if (aThing === undefined) {

return "Message: " + aThing;

if (typeof aThing == "object") {

let type = getCtorName(aThing);

if (Element.isInstance(aThing)) {

return debugElement(aThing);

type = type == "Object" ? "" : type + " ";

json = JSON.stringify(aThing);

// Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled

json = "{" + Object.keys(aThing).join(":..,") + ":.., }";

if (typeof aThing == "function") {

return aThing.toString().replace(/\s+/g, " ");

let str = aThing.toString();

str = str.replace(/\n/g, "|");

* Create a simple debug representation of a given element.

* @param {Element} aElement

* A simple single line representation of aElement

function debugElement(aElement) {

(aElement.id ? "#" + aElement.id : "") +

(aElement.className && aElement.className.split

? "." + aElement.className.split(" ").join(" .")

* A multi line stringification of an object, designed for use by humans

* The object to be stringified

* A multi line representation of aThing

if (aThing === undefined) {

if (typeof aThing == "object") {

let type = getCtorName(aThing);

for (let [key, value] of aThing) {

reply += logProperty(key, value);

} else if (type == "Set") {

for (let value of aThing) {

reply += logProperty("" + i, value);

} else if (isError(aThing)) {

reply += " Message: " + aThing + "\n";

var frame = aThing.stack;

reply += " " + frame + "\n";

} else if (Element.isInstance(aThing)) {

reply += " " + debugElement(aThing) + "\n";

let keys = Object.getOwnPropertyNames(aThing);

keys.forEach(function (aProp) {

reply += logProperty(aProp, aThing[aProp]);

let properties = Object.keys(root);

properties.forEach(function (property) {

if (!(property in logged)) {

logged[property] = property;

reply += logProperty(property, aThing[property]);

root = Object.getPrototypeOf(root);

reply += " - prototype " + getCtorName(root) + "\n";

return " " + aThing.toString() + "\n";

* Helper for log() which converts a property/value pair into an output

* The name of the property to include in the output string

* Value assigned to aProp to be converted to a single line string

* Multi line output string describing the property/value pair

function logProperty(aProp, aValue) {

if (aProp == "stack" && typeof value == "string") {

let trace = parseStack(aValue);

reply += formatTrace(trace);

reply += " - " + aProp + " = " + stringify(aValue) + "\n";

* Helper to tell if a console message of `aLevel` type

* should be logged in stdout and sent to consoles given

* the current maximum log level being defined in `console.maxLogLevel`

* Console message log level

* @param {string} aMaxLevel {string}

* String identifier (See LOG_LEVELS for possible

* values) that allows to filter which messages

* are logged based on their log level

* Should this message be logged or not?

function shouldLog(aLevel, aMaxLevel) {

return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel];

* Parse a stack trace, returning an array of stack frame objects, where

* each has filename/lineNumber/functionName members

* The serialized stack trace

* Array of { file: "...", line: NNN, call: "..." } objects

function parseStack(aStack) {

aStack.split("\n").forEach(function (line) {

let at = line.lastIndexOf("@");

let posn = line.substring(at + 1);

filename: posn.split(":")[0],

lineNumber: posn.split(":")[1],

functionName: line.substring(0, at),

* Format a frame coming from Components.stack such that it can be used by the

* Browser Console, via ConsoleAPIStorage notifications.

* The stack frame from which to begin the walk.

* @param {number=0} aMaxDepth

* Maximum stack trace depth. Default is 0 - no depth limit.

* An array of {filename, lineNumber, functionName, language} objects.

* These objects follow the same format as other ConsoleAPIStorage

function getStack(aFrame, aMaxDepth = 0) {

aFrame = Components.stack.caller;

filename: aFrame.filename,

lineNumber: aFrame.lineNumber,

functionName: aFrame.name,

language: aFrame.language,

if (aMaxDepth == trace.length) {

* Take the output from parseStack() and convert it to nice readable

* @param {object[]} aTrace

* Array of trace objects as created by parseStack()

* @return {string} Multi line report of the stack trace

function formatTrace(aTrace) {

aTrace.forEach(function (frame) {

fmt(frame.filename, 20, 20, { truncate: "start" }) +

fmt(frame.lineNumber, 5, 5) +

fmt(frame.functionName, 75, 0, { truncate: "center" }) +

* Create a new timer by recording the current time under the specified name.

* @param {number} [aTimestamp=Date.now()]

* Optional timestamp that tells when the timer was originally started.

* The name property holds the timer name and the started property

* holds the time the timer was started. In case of error, it returns

* an object with the single property "error" that contains the key

* for retrieving the localized error message.

function startTimer(aName, aTimestamp) {

let key = aName.toString();

if (!gTimerRegistry.has(key)) {

gTimerRegistry.set(key, aTimestamp || Date.now());

return { name: aName, started: gTimerRegistry.get(key) };

* Stop the timer with the specified name and retrieve the elapsed time.

* @param {number} [aTimestamp=Date.now()]

* Optional timestamp that tells when the timer was originally stopped.

* The name property holds the timer name and the duration property

* holds the number of milliseconds since the timer was started.

function stopTimer(aName, aTimestamp) {

let key = aName.toString();

let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key);

gTimerRegistry.delete(key);

return { name: aName, duration };

* Dump a new message header to stdout by taking care of adding an eventual

* @param {object} aConsole

* The string identifier for the message log level

* @param {string} aMessage

* The string message to print to stdout

function dumpMessage(aConsole, aLevel, aMessage) {

(aConsole.prefix ? aConsole.prefix + ": " : "") +

* Create a function which will output a concise level of output when used

* A prefix to all output generated from this function detailing the

* level at which output occurred

* @see createMultiLineDumper()

function createDumper(aLevel) {

if (!shouldLog(aLevel, this.maxLogLevel)) {

let args = Array.prototype.slice.call(arguments, 0);

let frame = getStack(Components.stack.caller, 1)[0];

sendConsoleAPIMessage(this, aLevel, frame, args);

let data = args.map(function (arg) {

return stringify(arg, true);

dumpMessage(this, aLevel, data.join(" "));

* Create a function which will output more detailed level of output when

* used as a logging function

* A prefix to all output generated from this function detailing the

* level at which output occurred

function createMultiLineDumper(aLevel) {

if (!shouldLog(aLevel, this.maxLogLevel)) {

dumpMessage(this, aLevel, "");

let args = Array.prototype.slice.call(arguments, 0);

let frame = getStack(Components.stack.caller, 1)[0];

sendConsoleAPIMessage(this, aLevel, frame, args);

args.forEach(function (arg) {

* Send a Console API message. This function will send a notification through

* the nsIConsoleAPIStorage service.

* @param {object} aConsole

* The instance of ConsoleAPI performing the logging.

* Message severity level. This is usually the name of the console method

* The youngest stack frame coming from Components.stack, as formatted by

* The arguments given to the console method.

* @param {object} aOptions

* Object properties depend on the console method that was invoked:

* - timer: for time() and timeEnd(). Holds the timer information.

* - groupName: for group(), groupCollapsed() and groupEnd().

* - stacktrace: for trace(). Holds the array of stack frames as given by

function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) {

innerID: aConsole.innerID || aFrame.filename,

consoleID: aConsole.consoleID,

filename: aFrame.filename,

lineNumber: aFrame.lineNumber,

functionName: aFrame.functionName,

consoleEvent.wrappedJSObject = consoleEvent;

consoleEvent.stacktrace = aOptions.stacktrace;

consoleEvent.timer = aOptions.timer;

consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");

let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(

ConsoleAPIStorage.recordEvent("jsm", consoleEvent);

* This creates a console object that somewhat replicates Firebug's console

* @param {object} aConsoleOptions

* Optional dictionary with a set of runtime console options:

* - prefix {string} : An optional prefix string to be printed before

* the actual logged message

* - maxLogLevel {string} : String identifier (See LOG_LEVELS for

* possible values) that allows to filter which

* messages are logged based on their log level.

* If falsy value, all messages will be logged.

* If wrong value that doesn't match any key of

* LOG_LEVELS, no message will be logged

* - maxLogLevelPref {string} : String pref name which contains the

* level to use for maxLogLevel. If the pref doesn't

* exist or gets removed, the maxLogLevel will default

* to the value passed to this constructor (or "all"

* if it wasn't specified).

* - dump {function} : An optional function to intercept all strings

* - innerID {string}: An ID representing the source of the message.

* Normally the inner ID of a DOM window.

* - consoleID {string} : String identified for the console, this will

* be passed through the console notifications

* A console API instance object

export function ConsoleAPI(aConsoleOptions = {}) {

// Normalize console options to set default values

// in order to avoid runtime checks on each console method call.

this.dump = aConsoleOptions.dump || dump;

this.prefix = aConsoleOptions.prefix || "";

this.maxLogLevel = aConsoleOptions.maxLogLevel;

this.innerID = aConsoleOptions.innerID || null;

this.consoleID = aConsoleOptions.consoleID || "";

// Setup maxLogLevelPref watching

let updateMaxLogLevel = () => {

Services.prefs.getPrefType(aConsoleOptions.maxLogLevelPref) ==

Services.prefs.PREF_STRING

this._maxLogLevel = Services.prefs

.getCharPref(aConsoleOptions.maxLogLevelPref)

this._maxLogLevel = this._maxExplicitLogLevel;

if (aConsoleOptions.maxLogLevelPref) {

Services.prefs.addObserver(

aConsoleOptions.maxLogLevelPref,

// Bind all the functions to this object.

if (typeof this[prop] === "function") {

this[prop] = this[prop].bind(this);

* The last log level that was specified via the constructor or setter. This

* is used as a fallback if the pref doesn't exist or is removed.

_maxExplicitLogLevel: null,

* The current log level via all methods of setting (pref or via the API).

debug: createMultiLineDumper("debug"),

assert: createDumper("assert"),

log: createDumper("log"),

info: createDumper("info"),

warn: createDumper("warn"),

error: createMultiLineDumper("error"),

exception: createMultiLineDumper("error"),

trace: function Console_trace() {

if (!shouldLog("trace", this.maxLogLevel)) {

let args = Array.prototype.slice.call(arguments, 0);

let trace = getStack(Components.stack.caller);

sendConsoleAPIMessage(this, "trace", trace[0], args, { stacktrace: trace });

dumpMessage(this, "trace", "\n" + formatTrace(trace));

clear: function Console_clear() {},

dir: createMultiLineDumper("dir"),

dirxml: createMultiLineDumper("dirxml"),

group: createDumper("group"),

groupEnd: createDumper("groupEnd"),

time: function Console_time() {

if (!shouldLog("time", this.maxLogLevel)) {

let args = Array.prototype.slice.call(arguments, 0);

let frame = getStack(Components.stack.caller, 1)[0];

let timer = startTimer(args[0]);

sendConsoleAPIMessage(this, "time", frame, args, { timer });

dumpMessage(this, "time", "'" + timer.name + "' @ " + new Date());

timeEnd: function Console_timeEnd() {

if (!shouldLog("timeEnd", this.maxLogLevel)) {

let args = Array.prototype.slice.call(arguments, 0);

let frame = getStack(Components.stack.caller, 1)[0];

let timer = stopTimer(args[0]);

sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer });

"'" + timer.name + "' " + timer.duration + "ms"

if (!shouldLog("profile", this.maxLogLevel)) {

Services.obs.notifyObservers(

arguments: [profileName],

dumpMessage(this, "profile", `'${profileName}'`);

profileEnd(profileName) {

if (!shouldLog("profileEnd", this.maxLogLevel)) {

Services.obs.notifyObservers(

arguments: [profileName],

dumpMessage(this, "profileEnd", `'${profileName}'`);

return this._maxLogLevel || "all";

set maxLogLevel(aValue) {

this._maxLogLevel = this._maxExplicitLogLevel = aValue;

return shouldLog(aLevel, this.maxLogLevel);