michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Log"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; michael@0: michael@0: const ONE_BYTE = 1; michael@0: const ONE_KILOBYTE = 1024 * ONE_BYTE; michael@0: const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; michael@0: michael@0: const STREAM_SEGMENT_SIZE = 4096; michael@0: const PR_UINT32_MAX = 0xffffffff; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]); michael@0: michael@0: michael@0: /* michael@0: * Dump a message everywhere we can if we have a failure. michael@0: */ michael@0: function dumpError(text) { michael@0: dump(text + "\n"); michael@0: Cu.reportError(text); michael@0: } michael@0: michael@0: this.Log = { michael@0: Level: { michael@0: Fatal: 70, michael@0: Error: 60, michael@0: Warn: 50, michael@0: Info: 40, michael@0: Config: 30, michael@0: Debug: 20, michael@0: Trace: 10, michael@0: All: 0, michael@0: Desc: { michael@0: 70: "FATAL", michael@0: 60: "ERROR", michael@0: 50: "WARN", michael@0: 40: "INFO", michael@0: 30: "CONFIG", michael@0: 20: "DEBUG", michael@0: 10: "TRACE", michael@0: 0: "ALL" michael@0: }, michael@0: Numbers: { michael@0: "FATAL": 70, michael@0: "ERROR": 60, michael@0: "WARN": 50, michael@0: "INFO": 40, michael@0: "CONFIG": 30, michael@0: "DEBUG": 20, michael@0: "TRACE": 10, michael@0: "ALL": 0, michael@0: } michael@0: }, michael@0: michael@0: get repository() { michael@0: delete Log.repository; michael@0: Log.repository = new LoggerRepository(); michael@0: return Log.repository; michael@0: }, michael@0: set repository(value) { michael@0: delete Log.repository; michael@0: Log.repository = value; michael@0: }, michael@0: michael@0: LogMessage: LogMessage, michael@0: Logger: Logger, michael@0: LoggerRepository: LoggerRepository, michael@0: michael@0: Formatter: Formatter, michael@0: BasicFormatter: BasicFormatter, michael@0: MessageOnlyFormatter: MessageOnlyFormatter, michael@0: StructuredFormatter: StructuredFormatter, michael@0: michael@0: Appender: Appender, michael@0: DumpAppender: DumpAppender, michael@0: ConsoleAppender: ConsoleAppender, michael@0: StorageStreamAppender: StorageStreamAppender, michael@0: michael@0: FileAppender: FileAppender, michael@0: BoundedFileAppender: BoundedFileAppender, michael@0: michael@0: ParameterFormatter: ParameterFormatter, michael@0: // Logging helper: michael@0: // let logger = Log.repository.getLogger("foo"); michael@0: // logger.info(Log.enumerateInterfaces(someObject).join(",")); michael@0: enumerateInterfaces: function Log_enumerateInterfaces(aObject) { michael@0: let interfaces = []; michael@0: michael@0: for (i in Ci) { michael@0: try { michael@0: aObject.QueryInterface(Ci[i]); michael@0: interfaces.push(i); michael@0: } michael@0: catch(ex) {} michael@0: } michael@0: michael@0: return interfaces; michael@0: }, michael@0: michael@0: // Logging helper: michael@0: // let logger = Log.repository.getLogger("foo"); michael@0: // logger.info(Log.enumerateProperties(someObject).join(",")); michael@0: enumerateProperties: function (aObject, aExcludeComplexTypes) { michael@0: let properties = []; michael@0: michael@0: for (p in aObject) { michael@0: try { michael@0: if (aExcludeComplexTypes && michael@0: (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function")) michael@0: continue; michael@0: properties.push(p + " = " + aObject[p]); michael@0: } michael@0: catch(ex) { michael@0: properties.push(p + " = " + ex); michael@0: } michael@0: } michael@0: michael@0: return properties; michael@0: }, michael@0: michael@0: _formatError: function _formatError(e) { michael@0: let result = e.toString(); michael@0: if (e.fileName) { michael@0: result += " (" + e.fileName; michael@0: if (e.lineNumber) { michael@0: result += ":" + e.lineNumber; michael@0: } michael@0: if (e.columnNumber) { michael@0: result += ":" + e.columnNumber; michael@0: } michael@0: result += ")"; michael@0: } michael@0: return result + " " + Log.stackTrace(e); michael@0: }, michael@0: michael@0: // This is for back compatibility with services/common/utils.js; we duplicate michael@0: // some of the logic in ParameterFormatter michael@0: exceptionStr: function exceptionStr(e) { michael@0: if (!e) { michael@0: return "" + e; michael@0: } michael@0: if (e instanceof Ci.nsIException) { michael@0: return e.toString() + " " + Log.stackTrace(e); michael@0: } michael@0: else if (isError(e)) { michael@0: return Log._formatError(e); michael@0: } michael@0: // else michael@0: let message = e.message ? e.message : e; michael@0: return message + " " + Log.stackTrace(e); michael@0: }, michael@0: michael@0: stackTrace: function stackTrace(e) { michael@0: // Wrapped nsIException michael@0: if (e.location) { michael@0: let frame = e.location; michael@0: let output = []; michael@0: while (frame) { michael@0: // Works on frames or exceptions, munges file:// URIs to shorten the paths michael@0: // FIXME: filename munging is sort of hackish, might be confusing if michael@0: // there are multiple extensions with similar filenames michael@0: let str = ""; michael@0: michael@0: let file = frame.filename || frame.fileName; michael@0: if (file) { michael@0: str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1"); michael@0: } michael@0: michael@0: if (frame.lineNumber) { michael@0: str += ":" + frame.lineNumber; michael@0: } michael@0: michael@0: if (frame.name) { michael@0: str = frame.name + "()@" + str; michael@0: } michael@0: michael@0: if (str) { michael@0: output.push(str); michael@0: } michael@0: frame = frame.caller; michael@0: } michael@0: return "Stack trace: " + output.join(" < "); michael@0: } michael@0: // Standard JS exception michael@0: if (e.stack) { michael@0: return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). michael@0: replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1"); michael@0: } michael@0: michael@0: return "No traceback available"; michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * LogMessage michael@0: * Encapsulates a single log event's data michael@0: */ michael@0: function LogMessage(loggerName, level, message, params) { michael@0: this.loggerName = loggerName; michael@0: this.level = level; michael@0: /* michael@0: * Special case to handle "log./level/(object)", for example logging a caught exception michael@0: * without providing text or params like: catch(e) { logger.warn(e) } michael@0: * Treating this as an empty text with the object in the 'params' field causes the michael@0: * object to be formatted properly by BasicFormatter. michael@0: */ michael@0: if (!params && message && (typeof(message) == "object") && michael@0: (typeof(message.valueOf()) != "string")) { michael@0: this.message = null; michael@0: this.params = message; michael@0: } else { michael@0: // If the message text is empty, or a string, or a String object, normal handling michael@0: this.message = message; michael@0: this.params = params; michael@0: } michael@0: michael@0: // The _structured field will correspond to whether this message is to michael@0: // be interpreted as a structured message. michael@0: this._structured = this.params && this.params.action; michael@0: this.time = Date.now(); michael@0: } michael@0: LogMessage.prototype = { michael@0: get levelDesc() { michael@0: if (this.level in Log.Level.Desc) michael@0: return Log.Level.Desc[this.level]; michael@0: return "UNKNOWN"; michael@0: }, michael@0: michael@0: toString: function LogMsg_toString() { michael@0: let msg = "LogMessage [" + this.time + " " + this.level + " " + michael@0: this.message; michael@0: if (this.params) { michael@0: msg += " " + JSON.stringify(this.params); michael@0: } michael@0: return msg + "]" michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * Logger michael@0: * Hierarchical version. Logs to all appenders, assigned or inherited michael@0: */ michael@0: michael@0: function Logger(name, repository) { michael@0: if (!repository) michael@0: repository = Log.repository; michael@0: this._name = name; michael@0: this.children = []; michael@0: this.ownAppenders = []; michael@0: this.appenders = []; michael@0: this._repository = repository; michael@0: } michael@0: Logger.prototype = { michael@0: get name() { michael@0: return this._name; michael@0: }, michael@0: michael@0: _level: null, michael@0: get level() { michael@0: if (this._level != null) michael@0: return this._level; michael@0: if (this.parent) michael@0: return this.parent.level; michael@0: dumpError("Log warning: root logger configuration error: no level defined"); michael@0: return Log.Level.All; michael@0: }, michael@0: set level(level) { michael@0: this._level = level; michael@0: }, michael@0: michael@0: _parent: null, michael@0: get parent() this._parent, michael@0: set parent(parent) { michael@0: if (this._parent == parent) { michael@0: return; michael@0: } michael@0: // Remove ourselves from parent's children michael@0: if (this._parent) { michael@0: let index = this._parent.children.indexOf(this); michael@0: if (index != -1) { michael@0: this._parent.children.splice(index, 1); michael@0: } michael@0: } michael@0: this._parent = parent; michael@0: parent.children.push(this); michael@0: this.updateAppenders(); michael@0: }, michael@0: michael@0: updateAppenders: function updateAppenders() { michael@0: if (this._parent) { michael@0: let notOwnAppenders = this._parent.appenders.filter(function(appender) { michael@0: return this.ownAppenders.indexOf(appender) == -1; michael@0: }, this); michael@0: this.appenders = notOwnAppenders.concat(this.ownAppenders); michael@0: } else { michael@0: this.appenders = this.ownAppenders.slice(); michael@0: } michael@0: michael@0: // Update children's appenders. michael@0: for (let i = 0; i < this.children.length; i++) { michael@0: this.children[i].updateAppenders(); michael@0: } michael@0: }, michael@0: michael@0: addAppender: function Logger_addAppender(appender) { michael@0: if (this.ownAppenders.indexOf(appender) != -1) { michael@0: return; michael@0: } michael@0: this.ownAppenders.push(appender); michael@0: this.updateAppenders(); michael@0: }, michael@0: michael@0: removeAppender: function Logger_removeAppender(appender) { michael@0: let index = this.ownAppenders.indexOf(appender); michael@0: if (index == -1) { michael@0: return; michael@0: } michael@0: this.ownAppenders.splice(index, 1); michael@0: this.updateAppenders(); michael@0: }, michael@0: michael@0: /** michael@0: * Logs a structured message object. michael@0: * michael@0: * @param action michael@0: * (string) A message action, one of a set of actions known to the michael@0: * log consumer. michael@0: * @param params michael@0: * (object) Parameters to be included in the message. michael@0: * If _level is included as a key and the corresponding value michael@0: * is a number or known level name, the message will be logged michael@0: * at the indicated level. If _message is included as a key, the michael@0: * value is used as the descriptive text for the message. michael@0: */ michael@0: logStructured: function (action, params) { michael@0: if (!action) { michael@0: throw "An action is required when logging a structured message."; michael@0: } michael@0: if (!params) { michael@0: return this.log(this.level, undefined, {"action": action}); michael@0: } michael@0: if (typeof(params) != "object") { michael@0: throw "The params argument is required to be an object."; michael@0: } michael@0: michael@0: let level = params._level; michael@0: if (level) { michael@0: let ulevel = level.toUpperCase(); michael@0: if (ulevel in Log.Level.Numbers) { michael@0: level = Log.Level.Numbers[ulevel]; michael@0: } michael@0: } else { michael@0: level = this.level; michael@0: } michael@0: michael@0: params.action = action; michael@0: this.log(level, params._message, params); michael@0: }, michael@0: michael@0: log: function (level, string, params) { michael@0: if (this.level > level) michael@0: return; michael@0: michael@0: // Hold off on creating the message object until we actually have michael@0: // an appender that's responsible. michael@0: let message; michael@0: let appenders = this.appenders; michael@0: for (let appender of appenders) { michael@0: if (appender.level > level) { michael@0: continue; michael@0: } michael@0: if (!message) { michael@0: message = new LogMessage(this._name, level, string, params); michael@0: } michael@0: appender.append(message); michael@0: } michael@0: }, michael@0: michael@0: fatal: function (string, params) { michael@0: this.log(Log.Level.Fatal, string, params); michael@0: }, michael@0: error: function (string, params) { michael@0: this.log(Log.Level.Error, string, params); michael@0: }, michael@0: warn: function (string, params) { michael@0: this.log(Log.Level.Warn, string, params); michael@0: }, michael@0: info: function (string, params) { michael@0: this.log(Log.Level.Info, string, params); michael@0: }, michael@0: config: function (string, params) { michael@0: this.log(Log.Level.Config, string, params); michael@0: }, michael@0: debug: function (string, params) { michael@0: this.log(Log.Level.Debug, string, params); michael@0: }, michael@0: trace: function (string, params) { michael@0: this.log(Log.Level.Trace, string, params); michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * LoggerRepository michael@0: * Implements a hierarchy of Loggers michael@0: */ michael@0: michael@0: function LoggerRepository() {} michael@0: LoggerRepository.prototype = { michael@0: _loggers: {}, michael@0: michael@0: _rootLogger: null, michael@0: get rootLogger() { michael@0: if (!this._rootLogger) { michael@0: this._rootLogger = new Logger("root", this); michael@0: this._rootLogger.level = Log.Level.All; michael@0: } michael@0: return this._rootLogger; michael@0: }, michael@0: set rootLogger(logger) { michael@0: throw "Cannot change the root logger"; michael@0: }, michael@0: michael@0: _updateParents: function LogRep__updateParents(name) { michael@0: let pieces = name.split('.'); michael@0: let cur, parent; michael@0: michael@0: // find the closest parent michael@0: // don't test for the logger name itself, as there's a chance it's already michael@0: // there in this._loggers michael@0: for (let i = 0; i < pieces.length - 1; i++) { michael@0: if (cur) michael@0: cur += '.' + pieces[i]; michael@0: else michael@0: cur = pieces[i]; michael@0: if (cur in this._loggers) michael@0: parent = cur; michael@0: } michael@0: michael@0: // if we didn't assign a parent above, there is no parent michael@0: if (!parent) michael@0: this._loggers[name].parent = this.rootLogger; michael@0: else michael@0: this._loggers[name].parent = this._loggers[parent]; michael@0: michael@0: // trigger updates for any possible descendants of this logger michael@0: for (let logger in this._loggers) { michael@0: if (logger != name && logger.indexOf(name) == 0) michael@0: this._updateParents(logger); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a named Logger. michael@0: * michael@0: * The returned Logger instance for a particular name is shared among michael@0: * all callers. In other words, if two consumers call getLogger("foo"), michael@0: * they will both have a reference to the same object. michael@0: * michael@0: * @return Logger michael@0: */ michael@0: getLogger: function (name) { michael@0: if (name in this._loggers) michael@0: return this._loggers[name]; michael@0: this._loggers[name] = new Logger(name, this); michael@0: this._updateParents(name); michael@0: return this._loggers[name]; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a Logger that logs all string messages with a prefix. michael@0: * michael@0: * A common pattern is to have separate Logger instances for each instance michael@0: * of an object. But, you still want to distinguish between each instance. michael@0: * Since Log.repository.getLogger() returns shared Logger objects, michael@0: * monkeypatching one Logger modifies them all. michael@0: * michael@0: * This function returns a new object with a prototype chain that chains michael@0: * up to the original Logger instance. The new prototype has log functions michael@0: * that prefix content to each message. michael@0: * michael@0: * @param name michael@0: * (string) The Logger to retrieve. michael@0: * @param prefix michael@0: * (string) The string to prefix each logged message with. michael@0: */ michael@0: getLoggerWithMessagePrefix: function (name, prefix) { michael@0: let log = this.getLogger(name); michael@0: michael@0: let proxy = {__proto__: log}; michael@0: michael@0: for (let level in Log.Level) { michael@0: if (level == "Desc") { michael@0: continue; michael@0: } michael@0: michael@0: let lc = level.toLowerCase(); michael@0: proxy[lc] = function (msg, ...args) { michael@0: return log[lc].apply(log, [prefix + msg, ...args]); michael@0: }; michael@0: } michael@0: michael@0: return proxy; michael@0: }, michael@0: }; michael@0: michael@0: /* michael@0: * Formatters michael@0: * These massage a LogMessage into whatever output is desired. michael@0: * BasicFormatter and StructuredFormatter are implemented here. michael@0: */ michael@0: michael@0: // Abstract formatter michael@0: function Formatter() {} michael@0: Formatter.prototype = { michael@0: format: function Formatter_format(message) {} michael@0: }; michael@0: michael@0: // Basic formatter that doesn't do anything fancy. michael@0: function BasicFormatter(dateFormat) { michael@0: if (dateFormat) { michael@0: this.dateFormat = dateFormat; michael@0: } michael@0: this.parameterFormatter = new ParameterFormatter(); michael@0: } michael@0: BasicFormatter.prototype = { michael@0: __proto__: Formatter.prototype, michael@0: michael@0: /** michael@0: * Format the text of a message with optional parameters. michael@0: * If the text contains ${identifier}, replace that with michael@0: * the value of params[identifier]; if ${}, replace that with michael@0: * the entire params object. If no params have been substituted michael@0: * into the text, format the entire object and append that michael@0: * to the message. michael@0: */ michael@0: formatText: function (message) { michael@0: let params = message.params; michael@0: if (!params) { michael@0: return message.message || ""; michael@0: } michael@0: // Defensive handling of non-object params michael@0: // We could add a special case for NSRESULT values here... michael@0: let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function'); michael@0: michael@0: // if we have params, try and find substitutions. michael@0: if (message.params && this.parameterFormatter) { michael@0: // have we successfully substituted any parameters into the message? michael@0: // in the log message michael@0: let subDone = false; michael@0: let regex = /\$\{(\S*)\}/g; michael@0: let textParts = []; michael@0: if (message.message) { michael@0: textParts.push(message.message.replace(regex, (_, sub) => { michael@0: // ${foo} means use the params['foo'] michael@0: if (sub) { michael@0: if (pIsObject && sub in message.params) { michael@0: subDone = true; michael@0: return this.parameterFormatter.format(message.params[sub]); michael@0: } michael@0: return '${' + sub + '}'; michael@0: } michael@0: // ${} means use the entire params object. michael@0: subDone = true; michael@0: return this.parameterFormatter.format(message.params); michael@0: })); michael@0: } michael@0: if (!subDone) { michael@0: // There were no substitutions in the text, so format the entire params object michael@0: let rest = this.parameterFormatter.format(message.params); michael@0: if (rest !== null && rest != "{}") { michael@0: textParts.push(rest); michael@0: } michael@0: } michael@0: return textParts.join(': '); michael@0: } michael@0: }, michael@0: michael@0: format: function BF_format(message) { michael@0: return message.time + "\t" + michael@0: message.loggerName + "\t" + michael@0: message.levelDesc + "\t" + michael@0: this.formatText(message); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A formatter that only formats the string message component. michael@0: */ michael@0: function MessageOnlyFormatter() { michael@0: } michael@0: MessageOnlyFormatter.prototype = Object.freeze({ michael@0: __proto__: Formatter.prototype, michael@0: michael@0: format: function (message) { michael@0: return message.message; michael@0: }, michael@0: }); michael@0: michael@0: // Structured formatter that outputs JSON based on message data. michael@0: // This formatter will format unstructured messages by supplying michael@0: // default values. michael@0: function StructuredFormatter() { } michael@0: StructuredFormatter.prototype = { michael@0: __proto__: Formatter.prototype, michael@0: michael@0: format: function (logMessage) { michael@0: let output = { michael@0: _time: logMessage.time, michael@0: _namespace: logMessage.loggerName, michael@0: _level: logMessage.levelDesc michael@0: }; michael@0: michael@0: for (let key in logMessage.params) { michael@0: output[key] = logMessage.params[key]; michael@0: } michael@0: michael@0: if (!output.action) { michael@0: output.action = "UNKNOWN"; michael@0: } michael@0: michael@0: if (!output._message && logMessage.message) { michael@0: output._message = logMessage.message; michael@0: } michael@0: michael@0: return JSON.stringify(output); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Test an object to see if it is a Mozilla JS Error. michael@0: */ michael@0: function isError(aObj) { michael@0: return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj && michael@0: "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj); michael@0: }; michael@0: michael@0: /* michael@0: * Parameter Formatters michael@0: * These massage an object used as a parameter for a LogMessage into michael@0: * a string representation of the object. michael@0: */ michael@0: michael@0: function ParameterFormatter() { michael@0: this._name = "ParameterFormatter" michael@0: } michael@0: ParameterFormatter.prototype = { michael@0: format: function(ob) { michael@0: try { michael@0: if (ob === undefined) { michael@0: return "undefined"; michael@0: } michael@0: if (ob === null) { michael@0: return "null"; michael@0: } michael@0: // Pass through primitive types and objects that unbox to primitive types. michael@0: if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") && michael@0: typeof(ob) != "function") { michael@0: return ob; michael@0: } michael@0: if (ob instanceof Ci.nsIException) { michael@0: return ob.toString() + " " + Log.stackTrace(ob); michael@0: } michael@0: else if (isError(ob)) { michael@0: return Log._formatError(ob); michael@0: } michael@0: // Just JSONify it. Filter out our internal fields and those the caller has michael@0: // already handled. michael@0: return JSON.stringify(ob, (key, val) => { michael@0: if (INTERNAL_FIELDS.has(key)) { michael@0: return undefined; michael@0: } michael@0: return val; michael@0: }); michael@0: } michael@0: catch (e) { michael@0: dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e)); michael@0: } michael@0: // Fancy formatting failed. Just toSource() it - but even this may fail! michael@0: try { michael@0: return ob.toSource(); michael@0: } catch (_) { } michael@0: try { michael@0: return "" + ob; michael@0: } catch (_) { michael@0: return "[object]" michael@0: } michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Appenders michael@0: * These can be attached to Loggers to log to different places michael@0: * Simply subclass and override doAppend to implement a new one michael@0: */ michael@0: michael@0: function Appender(formatter) { michael@0: this._name = "Appender"; michael@0: this._formatter = formatter? formatter : new BasicFormatter(); michael@0: } michael@0: Appender.prototype = { michael@0: level: Log.Level.All, michael@0: michael@0: append: function App_append(message) { michael@0: if (message) { michael@0: this.doAppend(this._formatter.format(message)); michael@0: } michael@0: }, michael@0: toString: function App_toString() { michael@0: return this._name + " [level=" + this.level + michael@0: ", formatter=" + this._formatter + "]"; michael@0: }, michael@0: doAppend: function App_doAppend(formatted) {} michael@0: }; michael@0: michael@0: /* michael@0: * DumpAppender michael@0: * Logs to standard out michael@0: */ michael@0: michael@0: function DumpAppender(formatter) { michael@0: Appender.call(this, formatter); michael@0: this._name = "DumpAppender"; michael@0: } michael@0: DumpAppender.prototype = { michael@0: __proto__: Appender.prototype, michael@0: michael@0: doAppend: function DApp_doAppend(formatted) { michael@0: dump(formatted + "\n"); michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * ConsoleAppender michael@0: * Logs to the javascript console michael@0: */ michael@0: michael@0: function ConsoleAppender(formatter) { michael@0: Appender.call(this, formatter); michael@0: this._name = "ConsoleAppender"; michael@0: } michael@0: ConsoleAppender.prototype = { michael@0: __proto__: Appender.prototype, michael@0: michael@0: // XXX this should be replaced with calls to the Browser Console michael@0: append: function App_append(message) { michael@0: if (message) { michael@0: let m = this._formatter.format(message); michael@0: if (message.level > Log.Level.Warn) { michael@0: Cu.reportError(m); michael@0: return; michael@0: } michael@0: this.doAppend(m); michael@0: } michael@0: }, michael@0: michael@0: doAppend: function CApp_doAppend(formatted) { michael@0: Cc["@mozilla.org/consoleservice;1"]. michael@0: getService(Ci.nsIConsoleService).logStringMessage(formatted); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Append to an nsIStorageStream michael@0: * michael@0: * This writes logging output to an in-memory stream which can later be read michael@0: * back as an nsIInputStream. It can be used to avoid expensive I/O operations michael@0: * during logging. Instead, one can periodically consume the input stream and michael@0: * e.g. write it to disk asynchronously. michael@0: */ michael@0: function StorageStreamAppender(formatter) { michael@0: Appender.call(this, formatter); michael@0: this._name = "StorageStreamAppender"; michael@0: } michael@0: michael@0: StorageStreamAppender.prototype = { michael@0: __proto__: Appender.prototype, michael@0: michael@0: _converterStream: null, // holds the nsIConverterOutputStream michael@0: _outputStream: null, // holds the underlying nsIOutputStream michael@0: michael@0: _ss: null, michael@0: michael@0: get outputStream() { michael@0: if (!this._outputStream) { michael@0: // First create a raw stream. We can bail out early if that fails. michael@0: this._outputStream = this.newOutputStream(); michael@0: if (!this._outputStream) { michael@0: return null; michael@0: } michael@0: michael@0: // Wrap the raw stream in an nsIConverterOutputStream. We can reuse michael@0: // the instance if we already have one. michael@0: if (!this._converterStream) { michael@0: this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"] michael@0: .createInstance(Ci.nsIConverterOutputStream); michael@0: } michael@0: this._converterStream.init( michael@0: this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE, michael@0: Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); michael@0: } michael@0: return this._converterStream; michael@0: }, michael@0: michael@0: newOutputStream: function newOutputStream() { michael@0: let ss = this._ss = Cc["@mozilla.org/storagestream;1"] michael@0: .createInstance(Ci.nsIStorageStream); michael@0: ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null); michael@0: return ss.getOutputStream(0); michael@0: }, michael@0: michael@0: getInputStream: function getInputStream() { michael@0: if (!this._ss) { michael@0: return null; michael@0: } michael@0: return this._ss.newInputStream(0); michael@0: }, michael@0: michael@0: reset: function reset() { michael@0: if (!this._outputStream) { michael@0: return; michael@0: } michael@0: this.outputStream.close(); michael@0: this._outputStream = null; michael@0: this._ss = null; michael@0: }, michael@0: michael@0: doAppend: function (formatted) { michael@0: if (!formatted) { michael@0: return; michael@0: } michael@0: try { michael@0: this.outputStream.writeString(formatted + "\n"); michael@0: } catch(ex) { michael@0: if (ex.result == Cr.NS_BASE_STREAM_CLOSED) { michael@0: // The underlying output stream is closed, so let's open a new one michael@0: // and try again. michael@0: this._outputStream = null; michael@0: } try { michael@0: this.outputStream.writeString(formatted + "\n"); michael@0: } catch (ex) { michael@0: // Ah well, we tried, but something seems to be hosed permanently. michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * File appender michael@0: * michael@0: * Writes output to file using OS.File. michael@0: */ michael@0: function FileAppender(path, formatter) { michael@0: Appender.call(this, formatter); michael@0: this._name = "FileAppender"; michael@0: this._encoder = new TextEncoder(); michael@0: this._path = path; michael@0: this._file = null; michael@0: this._fileReadyPromise = null; michael@0: michael@0: // This is a promise exposed for testing/debugging the logger itself. michael@0: this._lastWritePromise = null; michael@0: } michael@0: michael@0: FileAppender.prototype = { michael@0: __proto__: Appender.prototype, michael@0: michael@0: _openFile: function () { michael@0: return Task.spawn(function _openFile() { michael@0: try { michael@0: this._file = yield OS.File.open(this._path, michael@0: {truncate: true}); michael@0: } catch (err) { michael@0: if (err instanceof OS.File.Error) { michael@0: this._file = null; michael@0: } else { michael@0: throw err; michael@0: } michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _getFile: function() { michael@0: if (!this._fileReadyPromise) { michael@0: this._fileReadyPromise = this._openFile(); michael@0: return this._fileReadyPromise; michael@0: } michael@0: michael@0: return this._fileReadyPromise.then(_ => { michael@0: if (!this._file) { michael@0: return this._openFile(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: doAppend: function (formatted) { michael@0: let array = this._encoder.encode(formatted + "\n"); michael@0: if (this._file) { michael@0: this._lastWritePromise = this._file.write(array); michael@0: } else { michael@0: this._lastWritePromise = this._getFile().then(_ => { michael@0: this._fileReadyPromise = null; michael@0: if (this._file) { michael@0: return this._file.write(array); michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: reset: function () { michael@0: let fileClosePromise = this._file.close(); michael@0: return fileClosePromise.then(_ => { michael@0: this._file = null; michael@0: return OS.File.remove(this._path); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Bounded File appender michael@0: * michael@0: * Writes output to file using OS.File. After the total message size michael@0: * (as defined by formatted.length) exceeds maxSize, existing messages michael@0: * will be discarded, and subsequent writes will be appended to a new log file. michael@0: */ michael@0: function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) { michael@0: FileAppender.call(this, path, formatter); michael@0: this._name = "BoundedFileAppender"; michael@0: this._size = 0; michael@0: this._maxSize = maxSize; michael@0: this._closeFilePromise = null; michael@0: } michael@0: michael@0: BoundedFileAppender.prototype = { michael@0: __proto__: FileAppender.prototype, michael@0: michael@0: doAppend: function (formatted) { michael@0: if (!this._removeFilePromise) { michael@0: if (this._size < this._maxSize) { michael@0: this._size += formatted.length; michael@0: return FileAppender.prototype.doAppend.call(this, formatted); michael@0: } michael@0: this._removeFilePromise = this.reset(); michael@0: } michael@0: this._removeFilePromise.then(_ => { michael@0: this._removeFilePromise = null; michael@0: this.doAppend(formatted); michael@0: }); michael@0: }, michael@0: michael@0: reset: function () { michael@0: let fileClosePromise; michael@0: if (this._fileReadyPromise) { michael@0: // An attempt to open the file may still be in progress. michael@0: fileClosePromise = this._fileReadyPromise.then(_ => { michael@0: return this._file.close(); michael@0: }); michael@0: } else { michael@0: fileClosePromise = this._file.close(); michael@0: } michael@0: michael@0: return fileClosePromise.then(_ => { michael@0: this._size = 0; michael@0: this._file = null; michael@0: return OS.File.remove(this._path); michael@0: }); michael@0: } michael@0: }; michael@0: