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