gecko/toolkit/modules/Log.jsm
Gregory Szorc 4198e2c13b Bug 989137 - Part 3: Log.jsm API to get a Logger that prefixes messages; r=bsmedberg
A common pattern for logging is to have multiple loggers for multiple
underlying object instances. You often want to have each instance attach
some identifying metdata contained in each logged message. This patch
provides an API to facilitate that.

--HG--
extra : rebase_source : 5816e0671c78f55cca45bdd1aed52c85695945c4
2014-03-28 11:36:37 -07:00

759 lines
19 KiB
JavaScript

/* 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");
this.Log = {
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 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,
// 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 Log_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 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;
dump("Log warning: root logger configuration error: no level defined\n");
return Log.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 Log.Level.Numbers) {
level = Log.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(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 = {__proto__: log};
for (let level in Log.Level) {
if (level == "Desc") {
continue;
}
let lc = level.toLowerCase();
proxy[lc] = function (msg, ...args) {
return log[lc].apply(log, [prefix + msg, ...args]);
};
}
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;
}
BasicFormatter.prototype = {
__proto__: Formatter.prototype,
format: function BF_format(message) {
return message.time + "\t" +
message.loggerName + "\t" +
message.levelDesc + "\t" +
message.message + "\n";
}
};
/**
* A formatter that only formats the string message component.
*/
function MessageOnlyFormatter() {
}
MessageOnlyFormatter.prototype = Object.freeze({
__proto__: Formatter.prototype,
format: function (message) {
return 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: 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(message) {}
};
/*
* DumpAppender
* Logs to standard out
*/
function DumpAppender(formatter) {
Appender.call(this, formatter);
this._name = "DumpAppender";
}
DumpAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function DApp_doAppend(message) {
dump(message);
}
};
/*
* ConsoleAppender
* Logs to the javascript console
*/
function ConsoleAppender(formatter) {
Appender.call(this, formatter);
this._name = "ConsoleAppender";
}
ConsoleAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function CApp_doAppend(message) {
if (message.level > Log.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) {
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 (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) {
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;
}
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) {
FileAppender.call(this, path, formatter);
this._name = "BoundedFileAppender";
this._size = 0;
this._maxSize = maxSize;
this._closeFilePromise = null;
}
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);
});
}
};