blob: b71f253aefd8be69435c1b639b817cc8424215c8 [file] [log] [blame]
// Copyright 2012 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/** @fileoverview Defines logging for the Firefox driver. */
goog.provide('fxdriver.logging');
goog.provide('fxdriver.logging.LoggingPreferences');
goog.require('fxdriver.files.File');
goog.require('fxdriver.prefs');
goog.require('goog.array');
goog.require('goog.debug.Formatter');
goog.require('goog.debug.Logger');
goog.require('goog.debug.TextFormatter');
goog.require('goog.object');
goog.require('goog.string');
/**
* Represents the logging preferences as sent across the wire.
*
* @typedef {{driver: (number|undefined), profiler: (number|undefined)}}
*/
fxdriver.logging.LoggingPreferences;
/**
* Logging levels.
* @enum {{value:number, name:string}}
*/
fxdriver.logging.LogLevel = {
ALL: { value: -Math.pow(2, 31), name: 'ALL' },
DEBUG: { value: 500, name: 'DEBUG' },
INFO: { value: 800, name: 'INFO' },
WARNING: { value: 900, name: 'WARNING' },
SEVERE: { value: 1000, name: 'SEVERE' },
OFF: { value: Math.pow(2, 31) - 1, name: 'OFF' }
};
/**
* Types of logs.
* @enum {string}
*/
fxdriver.logging.LogType = {
BROWSER: 'browser',
DRIVER: 'driver',
PROFILER: 'profiler'
};
/**
* A single log entry.
*
* @constructor
* @param {!fxdriver.logging.LogLevel} level The log level of the entry.
* @param {string} message The message of the entry.
*/
fxdriver.logging.LogEntry = function(level, message) {
/** @type {number} */
this.timestamp = new Date().getTime();
/** @type {!LogLevel} */
this.level = level;
/** @type {string} */
this.message = message;
};
/**
* Logs a debug message to the driver log.
*
* @param {string} message The message to log.
*/
fxdriver.logging.debug = function(message) {
fxdriver.logging.initialize_();
fxdriver.logging.getDriverLogger_().fine(message);
};
/**
* Logs an info message to the driver log.
*
* @param {string} message The message to log.
*/
fxdriver.logging.info = function(message) {
fxdriver.logging.initialize_();
fxdriver.logging.getDriverLogger_().info(message);
};
/**
* Logs a warning message to the driver log.
*
* @param {string} message The message to log.
*/
fxdriver.logging.warning = function(message) {
fxdriver.logging.initialize_();
fxdriver.logging.getDriverLogger_().warning(message);
};
/**
* Logs an error message to the driver log.
*
* @param {string} message The message to log.
*/
fxdriver.logging.error = function(message) {
fxdriver.logging.initialize_();
fxdriver.logging.getDriverLogger_().severe(message);
};
/**
* Logs a message on the given log level and for the given type.
*
* @param {!LogType} logType The log type to use.
* @param {!LogLevel} logLevel The log level to use.
* @param {string} message The message to log.
*/
fxdriver.logging.log = function(logType, logLevel, message) {
fxdriver.logging.initialize_();
if (typeof(message) != 'string') {
message = JSON.stringify(message);
}
var logFile = fxdriver.logging.getLogFile_(logType);
var entry = new fxdriver.logging.LogEntry(logLevel.name, message);
var logEntryStr = JSON.stringify(entry);
logEntryStr = logEntryStr.replace(/\n/g, ' ');
logFile.append(logEntryStr + '\n');
};
/**
* Fetches the webdriver closure logger.
*
* @private
* @return {!goog.debug.Logger} The logger.
*/
fxdriver.logging.getDriverLogger_ = function() {
return goog.debug.Logger.getLogger('webdriver');
};
/**
* Get log entries for a given log type.
*
* @param {string} logType The log type.
* @return {!Array.<!fxdriver.logging.LogEntry>} The log entries for the type.
*/
fxdriver.logging.getLog = function(logType) {
fxdriver.logging.initialize_();
if (fxdriver.logging.shouldIgnoreLogType_(logType)) {
return [];
}
return fxdriver.logging.filterLogEntries_(logType,
fxdriver.logging.getLogFromFile_(logType));
};
/**
* Get available log types.
*
* @return {!Array.<string>} The available log types.
*/
fxdriver.logging.getAvailableLogTypes = function() {
var result = [];
var collectLogTypes = function(value, key) {
if (!fxdriver.logging.shouldIgnoreLogType_(value)) {
result.push(value);
}
};
goog.object.forEach(fxdriver.logging.LogType, collectLogTypes);
return result;
};
/**
* Configures logging using logging preferences.
*
* @param {(string|!fxdriver.logging.LoggingPreferences)=} logging_prefs
* The preferences to use.
* @param {boolean} enable_profiler Whether to collect profiler log.
*/
fxdriver.logging.configure = function(logging_prefs, enable_profiler) {
fxdriver.logging.initialize_();
if (goog.isString(logging_prefs)) {
logging_prefs = JSON.parse(logging_prefs);
}
var configureLogLevels = function(logLevel, logType) {
if (logLevel == fxdriver.logging.LogLevel.OFF.name) {
fxdriver.logging.ignoreLogType_(logType);
} else {
fxdriver.logging.setLogLevel_(logType, logLevel);
}
};
goog.object.forEach(logging_prefs, configureLogLevels);
if (!enable_profiler) {
fxdriver.logging.ignoreLogType_(fxdriver.logging.LogType.PROFILER);
}
};
/**
* Component initialization guard.
* @private
* @type {!boolean}
*/
fxdriver.logging.initialized_ = false;
/**
* Initializes logging.
*
* @private
*/
fxdriver.logging.initialize_ = function() {
if (fxdriver.logging.initialized_) {
return;
}
fxdriver.logging.initialized_ = true;
var rootLogger = goog.debug.Logger.getLogger('');
fxdriver.logging.addClosureToDriverFileLogger_(rootLogger);
fxdriver.logging.addClosureToConsoleLogger_(rootLogger);
fxdriver.logging.addClosureToFileLogger_(rootLogger);
if (fxdriver.logging.hasConsoleListenerBeenRegistered_()) {
return;
}
fxdriver.logging.setConsoleListenerToRegistered_();
fxdriver.logging.addConsoleToFileLogger_();
};
/**
* Adds a log handler to the logger that records entries to Firefox's error
* console to a temporary file using the log entry format of the wire protocol.
*
* @private
*/
fxdriver.logging.addConsoleToFileLogger_ = function() {
var consoleService = Components.classes['@mozilla.org/consoleservice;1']
.getService(Components.interfaces.nsIConsoleService);
var formatter = new fxdriver.logging.ConsoleToLogEntryFormatter();
// fetch already existing console entries
var file = fxdriver.logging.getLogFile_(fxdriver.logging.LogType.BROWSER);
var existingEntries = {};
consoleService.getMessageArray(existingEntries, {});
goog.array.forEach(existingEntries.value, function(consoleEntry) {
file.append(formatter.formatConsoleEntry(consoleEntry));
});
// add console listener
var filter = new fxdriver.logging.ConsoleToLogEntryFilter();
var file = fxdriver.logging.getLogFile_(fxdriver.logging.LogType.BROWSER);
var consoleListener = {
observe: function(consoleEntry) {
if (!filter.excludeConsoleEntry(consoleEntry)) {
file.append(formatter.formatConsoleEntry(consoleEntry));
}
}
};
consoleListener.QueryInterface = fxdriver.moz.queryInterface(consoleListener,
[Components.interfaces.nsIConsoleListener]);
consoleService.registerListener(consoleListener);
};
/**
* Adds a log handler to the logger that will record messages to
* Firefox's Error Console.
*
* @private
* @param {!goog.debug.Logger} logger The logger to use.
*/
fxdriver.logging.addClosureToConsoleLogger_ = function(logger) {
var formatter = new fxdriver.logging.ClosureToConsoleFormatter();
formatter.showSeverityLevel = true;
var consoleService = Components.classes['@mozilla.org/consoleservice;1']
.getService(Components.interfaces['nsIConsoleService']);
logger.addHandler(function(logRecord) {
consoleService.logStringMessage(formatter.formatRecord(logRecord));
});
};
/**
* Adds a log handler to the logger that will record to a log file.
*
* @private
* @param {!goog.debug.Logger} logger The logger to use.
*/
fxdriver.logging.addClosureToFileLogger_ = function(logger) {
var filePath = fxdriver.prefs.getCharPref(
fxdriver.logging.getPrefNameLogFile_(), undefined);
if (!filePath) {
return;
}
var formatter = new goog.debug.TextFormatter('webdriver');
if ('/dev/stdout' == filePath) {
logger.addHandler(function(logRecord) {
dump(formatter.formatRecord(logRecord));
});
return;
}
var file = fxdriver.files.getFile(filePath);
logger.addHandler(function(logRecord) {
file.append(formatter.formatRecord(logRecord));
});
};
/**
* Adds a log handler to the logger that records log entries to a temporary
* file using the log entry format of the wire protocol.
*
* @private
* @param {!goog.debug.Logger} logger The logger to use.
*/
fxdriver.logging.addClosureToDriverFileLogger_ = function(logger) {
var formatter = new fxdriver.logging.ClosureToLogEntryFormatter();
var file = fxdriver.logging.getLogFile_(fxdriver.logging.LogType.DRIVER);
logger.addHandler(function(logRecord) {
file.append(formatter.formatRecord(logRecord));
});
};
/**
* Filter log entries based on the log level set for the given log type.
*
* @private
* @param {!LogLevel} logType The log type.
* @param {!Array.<!fxdriver.logging.LogEntry>} logEntries The log entries
* to filter.
* @return {!Array.<!fxdriver.logging.LogEntry>} A filtered list of log entries.
*/
fxdriver.logging.filterLogEntries_ = function(logType, logEntries) {
var result = [];
var logLevel = fxdriver.logging.getLogLevel_(logType);
goog.array.forEach(logEntries, function(entry) {
if (entry.level.value >= logLevel.value) {
result.push(entry);
}
});
return result;
};
/**
* Filter deciding whether to exclude a console entry from being converted to
* a log entry.
*
* @constructor
*/
fxdriver.logging.ConsoleToLogEntryFilter = function() {
};
/**
* Checks if the given console entry should be excluded from being converted
* to a log entry.
*
* @param {!nsIConsoleMessage} consoleEntry The console entry.
* @return {!boolean} true if this entry should be excluded.
*/
fxdriver.logging.ConsoleToLogEntryFilter.prototype.excludeConsoleEntry =
function(consoleEntry) {
var msg = fxdriver.logging.getConsoleEntryMessage_(consoleEntry);
return goog.string.contains(msg, '[WEBDRIVER]');
};
/**
* Formatter that return a formatted JSON log entry to write to a file given
* a Firefox error console entry.
*
* @constructor
*/
fxdriver.logging.ConsoleToLogEntryFormatter = function() {
};
/**
* Formats a console entry to a JSON log entry.
*
* @param {!nsIConsoleMessage} consoleEntry The entry.
* @return {string} The formatted string.
*/
fxdriver.logging.ConsoleToLogEntryFormatter.prototype.formatConsoleEntry =
function(consoleEntry) {
var entry = {
level: fxdriver.logging.getConsoleEntryLogLevel_(consoleEntry).name,
message: fxdriver.logging.getConsoleEntryMessage_(consoleEntry),
timestamp: new Date().getTime()
};
return JSON.stringify(entry).replace(/\n/g, ' ') + '\n';
};
/**
* Gets the message from a browser log entry.
*
* @private
* @param {!nsIConsoleMessage} entry The log entry.
* @return {string} The message.
*/
fxdriver.logging.getConsoleEntryMessage_ =
function(entry) {
try {
entry.QueryInterface(Components.interfaces.nsIScriptError);
return entry.errorMessage;
} catch (interfaceNotSupported) {
}
try {
entry.QueryInterface(Components.interfaces.nsIConsoleMessage);
return entry.message + '\n';
} catch (interfaceNotSupported) {
}
return '' + entry;
};
/**
* Gets the log level from a browser log entry.
*
* @private
* @param {!nsIConsoleMessage} entry The log entry.
* @return {!fxdriver.logging.LogLevel} The log level.
*/
fxdriver.logging.getConsoleEntryLogLevel_ =
function(entry) {
try {
entry.QueryInterface(Components.interfaces.nsIScriptError);
if (entry.flags & entry.exceptionFlag) {
return fxdriver.logging.LogLevel.SEVERE;
}
if (entry.flags & entry.warningFlag) {
return fxdriver.logging.LogLevel.WARNING;
}
} catch (interfaceNotSupported) {
}
return fxdriver.logging.LogLevel.INFO;
};
/**
* Formatter that return a formatted JSON log entry to write to a file given
* a closure log record.
*
* @constructor
* @extends {goog.debug.Formatter}
*/
fxdriver.logging.ClosureToLogEntryFormatter = function() {
goog.debug.Formatter.call(this);
};
goog.inherits(fxdriver.logging.ClosureToLogEntryFormatter,
goog.debug.Formatter);
/**
* Formats a record as a JSON log entry.
*
* @param {goog.debug.LogRecord} logRecord the logRecord to format.
* @return {string} The formatted string.
*/
fxdriver.logging.ClosureToLogEntryFormatter.prototype.formatRecord =
function(logRecord) {
var entry = {
message: JSON.stringify(fxdriver.logging.formatMessage_(logRecord,
this.showExceptionText)),
level: fxdriver.logging.getLevelFromLogRecord_(logRecord.getLevel()).name,
timestamp: new Date().getTime()
};
return JSON.stringify(entry).replace(/\n/g, ' ') + '\n';
};
/**
* Translates goog.debug.Logger.Level to fxdriver.logging.LogLevel.
*
* @private
* @param {!goog.debug.Logger.Level} level The level to lookup.
* @return {!fxdriver.logging.LogLevel} the corresponding level in
* fxdriver.logging.LogLevel if found, otherwise returns
* fxdriver.logging.LogLevel.INFO.
*/
fxdriver.logging.getLevelFromLogRecord_ = function(level) {
switch (level) {
case goog.debug.Logger.Level.WARNING:
return fxdriver.logging.LogLevel.WARNING;
case goog.debug.Logger.Level.ERROR:
return fxdriver.logging.LogLevel.ERROR;
case goog.debug.Logger.Level.INFO:
return fxdriver.logging.LogLevel.INFO;
}
return fxdriver.logging.LogLevel.DEBUG;
};
/**
* Formatter that returns formatted text for display on Firefox's error console.
*
* @constructor
* @extends {goog.debug.Formatter}
*/
fxdriver.logging.ClosureToConsoleFormatter = function() {
goog.debug.Formatter.call(this);
};
goog.inherits(fxdriver.logging.ClosureToConsoleFormatter, goog.debug.Formatter);
/**
* Formats a record as text.
*
* @param {goog.debug.LogRecord} logRecord the logRecord to format.
* @return {string} The formatted string.
*/
fxdriver.logging.ClosureToConsoleFormatter.prototype.formatRecord =
function(logRecord) {
// Build message html
var sb = [];
sb.push('[WEBDRIVER] ');
if (this.showSeverityLevel) {
sb.push('[', logRecord.getLevel().name, '] ');
}
sb.push(fxdriver.logging.formatMessage_(logRecord, this.showExceptionText));
return sb.join('');
};
/**
* Formats a closure log record to a message.
*
* @private
* @param {!goog.debug.LogRecord} logRecord The log record.
* @param {boolean} showExceptionText Whether to show exception text.
* @return {string} The formatted message.
*/
fxdriver.logging.formatMessage_ = function(logRecord, showExceptionText) {
var sb = [];
var stack = Components.stack;
// Trial and error says that we're 8 levels deep in the stack when called.
for (var i = 0; i < 8 && stack; i++) {
stack = stack['caller'];
}
var filename = stack.filename.replace(/.*\//, '');
sb.push(filename + ':' + stack.lineNumber + ' ');
sb.push(logRecord.getMessage(), '\n');
if (showExceptionText && logRecord.getException()) {
sb.push(logRecord.getExceptionText(), '\n');
}
return sb.join('');
};
/**
* Creates a new log file for the given log type and stores its location.
*
* @private
* @param {!LogType} logType The log type.
* @return {!fxdriver.files.File} The new log file.
*/
fxdriver.logging.createNewLogFile_ = function(logType) {
var logFile = fxdriver.files.createTempFile(logType + '_log', '.txt');
fxdriver.prefs.setCharPref(fxdriver.logging.getPrefNameLogFile_(logType),
logFile.getFilePath());
return logFile;
};
/**
* Gets the log file for the given log type.
*
* @private
* @param {!LogType} logType The log type.
* @return {!fxdriver.logging.File} The log file.
*/
fxdriver.logging.getLogFile_ = function(logType) {
var fileName = fxdriver.prefs.getCharPref(
fxdriver.logging.getPrefNameLogFile_(logType), undefined);
var logFile = fxdriver.files.getFile(fileName);
if (!logFile) {
logFile = fxdriver.logging.createNewLogFile_(logType);
}
return logFile;
};
/**
* Gets log entries for the given log type.
*
* @private
* @param {!LogType} logType The log type.
* @return {!Array.<!fxdriver.logging.LogEntry>} All logged entries.
*/
fxdriver.logging.getLogFromFile_ = function(logType) {
// TODO: There is a risk of using up all available stack space when there
// is a large number of log entries. To prevent this from happening the
// number of returned log entries should be divided into chunks of some
// appropriate size, and for cases where something has been chunked off there
// should be an indication of this at the end of the list of log entries.
var logFile = fxdriver.logging.getLogFile_(logType);
var fileContent = logFile.read().trim().split('\n').join(',\n');
var result = JSON.parse('[' + fileContent + ']');
logFile.resetBuffer();
goog.array.forEach(result, function(entry) {
entry.level = fxdriver.logging.LogLevel[entry.level];
});
return result;
};
/**
* Sets the log level for the given log type.
*
* @private
* @param {!LogLevel} logType The log type.
* @param {string} logLevelName The log level name.
*/
fxdriver.logging.setLogLevel_ = function(logType, logLevelName) {
fxdriver.prefs.setCharPref(fxdriver.logging.getPrefNameLogLevel_(logType),
logLevelName);
};
/**
* Gets the log level for the given log type. Default is INFO if no level is
* stored for the log type.
*
* @private
* @param {!LogType} logType The log type.
* @return {!LogLevel} The log level.
*/
fxdriver.logging.getLogLevel_ = function(logType) {
return fxdriver.logging.LogLevel[fxdriver.prefs.getCharPref(
fxdriver.logging.getPrefNameLogLevel_(logType), 'INFO')];
};
/**
* Gets the log level preference name for the given log type.
*
* @private
* @param {!LogType} logType The log type to get the preference name for.
* @return {string} The preference name.
*/
fxdriver.logging.getPrefNameLogLevel_ = function(logType) {
return 'webdriver.log.' + logType + '.level';
};
/**
* Gets the log level preference name for the given log type, or for the named
* webdriver log file if a log type is not given.
*
* @private
* @param {LogType} logType The log type to get the preference name for.
* @return {string} The preference name.
*/
fxdriver.logging.getPrefNameLogFile_ = function(logType) {
if (!logType) {
return 'webdriver.log.file';
}
return 'webdriver.log.' + logType + '.file';
};
/**
* Checks if logging has been initialized.
*
* @private
* @return {!boolean} True if logging has been initialized.
*/
fxdriver.logging.hasConsoleListenerBeenRegistered_ = function() {
return fxdriver.prefs.getBoolPref(
fxdriver.logging.prefNameInitialized_, false);
};
/**
* Sets logging to initialized.
*
* @private
*/
fxdriver.logging.setConsoleListenerToRegistered_ = function() {
fxdriver.prefs.setBoolPref(fxdriver.logging.prefNameInitialized_, true);
};
/**
* Preference name used to keep track of logging initialization which should
* be run at most once.
*
* This preference acts as a guard which makes sure the protected code is only
* run once. This is needed for some parts of the initialization. For instance,
* there should only be one registered console listener or there will be
* duplicated messages in the logs.
*
* TODO: Find a better solution that doesn't use a preference.
*
* @private
* @type {string}
*/
fxdriver.logging.prefNameInitialized_ = 'webdriver.log.init';
/**
* Stores setting to ignore the given log type.
*
* @private
* @param {!LogType} logType The log type to ignore.
*/
fxdriver.logging.ignoreLogType_ = function(logType) {
fxdriver.prefs.setBoolPref(
fxdriver.logging.getPrefNameLogIgnore_(logType), true);
};
/**
* Checks if the given log type should be ignored.
*
* @private
* @param {!LogType} logType The log type to ignore.
* @return {!boolean} true if the log type should be ignored, otherwise false.
*/
fxdriver.logging.shouldIgnoreLogType_ = function(logType) {
return fxdriver.prefs.getBoolPref(
fxdriver.logging.getPrefNameLogIgnore_(logType), false);
};
/**
* Gets the log ignore preference name for the given log type.
*
* @private
* @param {!LogType} logType The log type to get the preference name for.
* @return {!string} The preference name.
*/
fxdriver.logging.getPrefNameLogIgnore_ = function(logType) {
return 'webdriver.log.' + logType + '.ignore';
};
/**
* Takes an object and attempts to discover which interfaces it implements.
*
* @param {*} object The object to dump.
*/
fxdriver.logging.dumpObject = function(element) {
var msg = '=============\n';
var rows = [];
msg += 'Supported interfaces: ';
for (var i in Components.interfaces) {
try {
var view = element.QueryInterface(Components.interfaces[i]);
msg += i + ', ';
} catch (e) {
// Doesn't support the interface
}
}
msg += '\n------------\n';
try {
fxdriver.logging.dumpProperties_(element, rows);
} catch (ignored) {
}
rows.sort();
for (var j in rows) {
msg += rows[j] + '\n';
}
msg += '=============\n\n\n';
fxdriver.logging.info(msg);
};
/**
* @param {*} view The object to get the properties of.
* @param {!Array.<string>} rows The place to output results to.
* @private
*/
fxdriver.logging.dumpProperties_ = function(view, rows) {
for (var i in view) {
var value = '\t' + i + ': ';
try {
if (typeof(view[i]) == typeof(Function)) {
value += ' function()';
} else {
value += String(view[i]);
}
} catch (e) {
value += ' Cannot obtain value';
}
rows.push(value);
}
};