From 2231b514e3646c6762818926b45b836e4263c0ea Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 28 Jan 2014 17:39:18 +0100 Subject: initial import --- chrome/content/log4moz.js | 698 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 chrome/content/log4moz.js (limited to 'chrome/content/log4moz.js') diff --git a/chrome/content/log4moz.js b/chrome/content/log4moz.js new file mode 100644 index 0000000..5e12938 --- /dev/null +++ b/chrome/content/log4moz.js @@ -0,0 +1,698 @@ +/* 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 = ['Log4Moz']; + +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"); + +this.Log4Moz = { + Level: { + Fatal: 70, + Error: 60, + Warn: 50, + Info: 40, + Config: 30, + Debug: 20, + Trace: 10, + All: 0, + Desc: { + 70: "FATAL", + 60: "ERROR", + 50: "WARN", + 40: "INFO", + 30: "CONFIG", + 20: "DEBUG", + 10: "TRACE", + 0: "ALL" + }, + Numbers: { + "FATAL": 70, + "ERROR": 60, + "WARN": 50, + "INFO": 40, + "CONFIG": 30, + "DEBUG": 20, + "TRACE": 10, + "ALL": 0, + } + }, + + get repository() { + delete Log4Moz.repository; + Log4Moz.repository = new LoggerRepository(); + return Log4Moz.repository; + }, + set repository(value) { + delete Log4Moz.repository; + Log4Moz.repository = value; + }, + + LogMessage: LogMessage, + Logger: Logger, + LoggerRepository: LoggerRepository, + + Formatter: Formatter, + BasicFormatter: BasicFormatter, + StructuredFormatter: StructuredFormatter, + + Appender: Appender, + DumpAppender: DumpAppender, + ConsoleAppender: ConsoleAppender, + StorageStreamAppender: StorageStreamAppender, + + FileAppender: FileAppender, + BoundedFileAppender: BoundedFileAppender, + + // Logging helper: + // let logger = Log4Moz.repository.getLogger("foo"); + // logger.info(Log4Moz.enumerateInterfaces(someObject).join(",")); + enumerateInterfaces: function Log4Moz_enumerateInterfaces(aObject) { + let interfaces = []; + + for (i in Ci) { + try { + aObject.QueryInterface(Ci[i]); + interfaces.push(i); + } + catch(ex) {} + } + + return interfaces; + }, + + // Logging helper: + // let logger = Log4Moz.repository.getLogger("foo"); + // logger.info(Log4Moz.enumerateProperties(someObject).join(",")); + enumerateProperties: function Log4Moz_enumerateProps(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; + } +}; + + +/* + * LogMessage + * Encapsulates a single log event's data + */ +function LogMessage(loggerName, level, message, params) { + this.loggerName = loggerName; + this.level = level; + 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 Log4Moz.Level.Desc) + return Log4Moz.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 = Log4Moz.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; + dump("log4moz warning: root logger configuration error: no level defined\n"); + return Log4Moz.Level.All; + }, + set level(level) { + this._level = level; + }, + + _parent: null, + get parent() 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. + */ + logStructured: function (action, params) { + if (!action) { + throw "An action is required when logging a structured message."; + } + if (!params) { + return this.log(this.level, undefined, {"action": action}); + } + if (typeof params != "object") { + throw "The params argument is required to be an object."; + } + + let level = params._level || this.level; + if ((typeof level == "string") && level in Log4Moz.Level.Numbers) { + level = Log4Moz.Level.Numbers[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(Log4Moz.Level.Fatal, string, params); + }, + error: function (string, params) { + this.log(Log4Moz.Level.Error, string, params); + }, + warn: function (string, params) { + this.log(Log4Moz.Level.Warn, string, params); + }, + info: function (string, params) { + this.log(Log4Moz.Level.Info, string, params); + }, + config: function (string, params) { + this.log(Log4Moz.Level.Config, string, params); + }, + debug: function (string, params) { + this.log(Log4Moz.Level.Debug, string, params); + }, + trace: function (string, params) { + this.log(Log4Moz.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 = Log4Moz.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); + } + }, + + getLogger: function LogRep_getLogger(name) { + if (name in this._loggers) + return this._loggers[name]; + this._loggers[name] = new Logger(name, this); + this._updateParents(name); + return this._loggers[name]; + } +}; + +/* + * 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; +} +BasicFormatter.prototype = { + __proto__: Formatter.prototype, + + format: function BF_format(message) { + return message.time + "\t" + + message.loggerName + "\t" + + message.levelDesc + "\t" + + message.message + "\n"; + } +}; + +// 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); + } +} + +/* + * 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: Log4Moz.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(message) {} +}; + +/* + * DumpAppender + * Logs to standard out + */ + +function DumpAppender(formatter) { + this._name = "DumpAppender"; + Appender.call(this, formatter); +} +DumpAppender.prototype = { + __proto__: Appender.prototype, + + doAppend: function DApp_doAppend(message) { + dump(message); + } +}; + +/* + * ConsoleAppender + * Logs to the javascript console + */ + +function ConsoleAppender(formatter) { + this._name = "ConsoleAppender"; + Appender.call(this, formatter); +} +ConsoleAppender.prototype = { + __proto__: Appender.prototype, + + doAppend: function CApp_doAppend(message) { + if (message.level > Log4Moz.Level.Warn) { + Cu.reportError(message); + return; + } + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService).logStringMessage(message); + } +}; + +/** + * 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) { + this._name = "StorageStreamAppender"; + Appender.call(this, formatter); +} + +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 (message) { + if (!message) { + return; + } + try { + this.outputStream.writeString(message); + } 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(message); + } 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) { + 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; + Appender.call(this, formatter); +} + +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; + } + + return this._fileReadyPromise.then(_ => { + if (!this._file) { + return this._openFile(); + } + }); + }, + + doAppend: function (message) { + let array = this._encoder.encode(message); + 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); + } + }); + } + }, + + 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 message.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) { + this._name = "BoundedFileAppender"; + this._size = 0; + this._maxSize = maxSize; + this._closeFilePromise = null; + FileAppender.call(this, path, formatter); +} + +BoundedFileAppender.prototype = { + __proto__: FileAppender.prototype, + + doAppend: function (message) { + if (!this._removeFilePromise) { + if (this._size < this._maxSize) { + this._size += message.length; + return FileAppender.prototype.doAppend.call(this, message); + } + this._removeFilePromise = this.reset(); + } + this._removeFilePromise.then(_ => { + this._removeFilePromise = null; + this.doAppend(message); + }); + }, + + 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); + }); + } +}; + -- cgit v1.2.3