1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/Log.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,979 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["Log"]; 1.11 + 1.12 +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; 1.13 + 1.14 +const ONE_BYTE = 1; 1.15 +const ONE_KILOBYTE = 1024 * ONE_BYTE; 1.16 +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; 1.17 + 1.18 +const STREAM_SEGMENT_SIZE = 4096; 1.19 +const PR_UINT32_MAX = 0xffffffff; 1.20 + 1.21 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.23 + "resource://gre/modules/osfile.jsm"); 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.25 + "resource://gre/modules/Task.jsm"); 1.26 +const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]); 1.27 + 1.28 + 1.29 +/* 1.30 + * Dump a message everywhere we can if we have a failure. 1.31 + */ 1.32 +function dumpError(text) { 1.33 + dump(text + "\n"); 1.34 + Cu.reportError(text); 1.35 +} 1.36 + 1.37 +this.Log = { 1.38 + Level: { 1.39 + Fatal: 70, 1.40 + Error: 60, 1.41 + Warn: 50, 1.42 + Info: 40, 1.43 + Config: 30, 1.44 + Debug: 20, 1.45 + Trace: 10, 1.46 + All: 0, 1.47 + Desc: { 1.48 + 70: "FATAL", 1.49 + 60: "ERROR", 1.50 + 50: "WARN", 1.51 + 40: "INFO", 1.52 + 30: "CONFIG", 1.53 + 20: "DEBUG", 1.54 + 10: "TRACE", 1.55 + 0: "ALL" 1.56 + }, 1.57 + Numbers: { 1.58 + "FATAL": 70, 1.59 + "ERROR": 60, 1.60 + "WARN": 50, 1.61 + "INFO": 40, 1.62 + "CONFIG": 30, 1.63 + "DEBUG": 20, 1.64 + "TRACE": 10, 1.65 + "ALL": 0, 1.66 + } 1.67 + }, 1.68 + 1.69 + get repository() { 1.70 + delete Log.repository; 1.71 + Log.repository = new LoggerRepository(); 1.72 + return Log.repository; 1.73 + }, 1.74 + set repository(value) { 1.75 + delete Log.repository; 1.76 + Log.repository = value; 1.77 + }, 1.78 + 1.79 + LogMessage: LogMessage, 1.80 + Logger: Logger, 1.81 + LoggerRepository: LoggerRepository, 1.82 + 1.83 + Formatter: Formatter, 1.84 + BasicFormatter: BasicFormatter, 1.85 + MessageOnlyFormatter: MessageOnlyFormatter, 1.86 + StructuredFormatter: StructuredFormatter, 1.87 + 1.88 + Appender: Appender, 1.89 + DumpAppender: DumpAppender, 1.90 + ConsoleAppender: ConsoleAppender, 1.91 + StorageStreamAppender: StorageStreamAppender, 1.92 + 1.93 + FileAppender: FileAppender, 1.94 + BoundedFileAppender: BoundedFileAppender, 1.95 + 1.96 + ParameterFormatter: ParameterFormatter, 1.97 + // Logging helper: 1.98 + // let logger = Log.repository.getLogger("foo"); 1.99 + // logger.info(Log.enumerateInterfaces(someObject).join(",")); 1.100 + enumerateInterfaces: function Log_enumerateInterfaces(aObject) { 1.101 + let interfaces = []; 1.102 + 1.103 + for (i in Ci) { 1.104 + try { 1.105 + aObject.QueryInterface(Ci[i]); 1.106 + interfaces.push(i); 1.107 + } 1.108 + catch(ex) {} 1.109 + } 1.110 + 1.111 + return interfaces; 1.112 + }, 1.113 + 1.114 + // Logging helper: 1.115 + // let logger = Log.repository.getLogger("foo"); 1.116 + // logger.info(Log.enumerateProperties(someObject).join(",")); 1.117 + enumerateProperties: function (aObject, aExcludeComplexTypes) { 1.118 + let properties = []; 1.119 + 1.120 + for (p in aObject) { 1.121 + try { 1.122 + if (aExcludeComplexTypes && 1.123 + (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function")) 1.124 + continue; 1.125 + properties.push(p + " = " + aObject[p]); 1.126 + } 1.127 + catch(ex) { 1.128 + properties.push(p + " = " + ex); 1.129 + } 1.130 + } 1.131 + 1.132 + return properties; 1.133 + }, 1.134 + 1.135 + _formatError: function _formatError(e) { 1.136 + let result = e.toString(); 1.137 + if (e.fileName) { 1.138 + result += " (" + e.fileName; 1.139 + if (e.lineNumber) { 1.140 + result += ":" + e.lineNumber; 1.141 + } 1.142 + if (e.columnNumber) { 1.143 + result += ":" + e.columnNumber; 1.144 + } 1.145 + result += ")"; 1.146 + } 1.147 + return result + " " + Log.stackTrace(e); 1.148 + }, 1.149 + 1.150 + // This is for back compatibility with services/common/utils.js; we duplicate 1.151 + // some of the logic in ParameterFormatter 1.152 + exceptionStr: function exceptionStr(e) { 1.153 + if (!e) { 1.154 + return "" + e; 1.155 + } 1.156 + if (e instanceof Ci.nsIException) { 1.157 + return e.toString() + " " + Log.stackTrace(e); 1.158 + } 1.159 + else if (isError(e)) { 1.160 + return Log._formatError(e); 1.161 + } 1.162 + // else 1.163 + let message = e.message ? e.message : e; 1.164 + return message + " " + Log.stackTrace(e); 1.165 + }, 1.166 + 1.167 + stackTrace: function stackTrace(e) { 1.168 + // Wrapped nsIException 1.169 + if (e.location) { 1.170 + let frame = e.location; 1.171 + let output = []; 1.172 + while (frame) { 1.173 + // Works on frames or exceptions, munges file:// URIs to shorten the paths 1.174 + // FIXME: filename munging is sort of hackish, might be confusing if 1.175 + // there are multiple extensions with similar filenames 1.176 + let str = "<file:unknown>"; 1.177 + 1.178 + let file = frame.filename || frame.fileName; 1.179 + if (file) { 1.180 + str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1"); 1.181 + } 1.182 + 1.183 + if (frame.lineNumber) { 1.184 + str += ":" + frame.lineNumber; 1.185 + } 1.186 + 1.187 + if (frame.name) { 1.188 + str = frame.name + "()@" + str; 1.189 + } 1.190 + 1.191 + if (str) { 1.192 + output.push(str); 1.193 + } 1.194 + frame = frame.caller; 1.195 + } 1.196 + return "Stack trace: " + output.join(" < "); 1.197 + } 1.198 + // Standard JS exception 1.199 + if (e.stack) { 1.200 + return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). 1.201 + replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1"); 1.202 + } 1.203 + 1.204 + return "No traceback available"; 1.205 + } 1.206 +}; 1.207 + 1.208 +/* 1.209 + * LogMessage 1.210 + * Encapsulates a single log event's data 1.211 + */ 1.212 +function LogMessage(loggerName, level, message, params) { 1.213 + this.loggerName = loggerName; 1.214 + this.level = level; 1.215 + /* 1.216 + * Special case to handle "log./level/(object)", for example logging a caught exception 1.217 + * without providing text or params like: catch(e) { logger.warn(e) } 1.218 + * Treating this as an empty text with the object in the 'params' field causes the 1.219 + * object to be formatted properly by BasicFormatter. 1.220 + */ 1.221 + if (!params && message && (typeof(message) == "object") && 1.222 + (typeof(message.valueOf()) != "string")) { 1.223 + this.message = null; 1.224 + this.params = message; 1.225 + } else { 1.226 + // If the message text is empty, or a string, or a String object, normal handling 1.227 + this.message = message; 1.228 + this.params = params; 1.229 + } 1.230 + 1.231 + // The _structured field will correspond to whether this message is to 1.232 + // be interpreted as a structured message. 1.233 + this._structured = this.params && this.params.action; 1.234 + this.time = Date.now(); 1.235 +} 1.236 +LogMessage.prototype = { 1.237 + get levelDesc() { 1.238 + if (this.level in Log.Level.Desc) 1.239 + return Log.Level.Desc[this.level]; 1.240 + return "UNKNOWN"; 1.241 + }, 1.242 + 1.243 + toString: function LogMsg_toString() { 1.244 + let msg = "LogMessage [" + this.time + " " + this.level + " " + 1.245 + this.message; 1.246 + if (this.params) { 1.247 + msg += " " + JSON.stringify(this.params); 1.248 + } 1.249 + return msg + "]" 1.250 + } 1.251 +}; 1.252 + 1.253 +/* 1.254 + * Logger 1.255 + * Hierarchical version. Logs to all appenders, assigned or inherited 1.256 + */ 1.257 + 1.258 +function Logger(name, repository) { 1.259 + if (!repository) 1.260 + repository = Log.repository; 1.261 + this._name = name; 1.262 + this.children = []; 1.263 + this.ownAppenders = []; 1.264 + this.appenders = []; 1.265 + this._repository = repository; 1.266 +} 1.267 +Logger.prototype = { 1.268 + get name() { 1.269 + return this._name; 1.270 + }, 1.271 + 1.272 + _level: null, 1.273 + get level() { 1.274 + if (this._level != null) 1.275 + return this._level; 1.276 + if (this.parent) 1.277 + return this.parent.level; 1.278 + dumpError("Log warning: root logger configuration error: no level defined"); 1.279 + return Log.Level.All; 1.280 + }, 1.281 + set level(level) { 1.282 + this._level = level; 1.283 + }, 1.284 + 1.285 + _parent: null, 1.286 + get parent() this._parent, 1.287 + set parent(parent) { 1.288 + if (this._parent == parent) { 1.289 + return; 1.290 + } 1.291 + // Remove ourselves from parent's children 1.292 + if (this._parent) { 1.293 + let index = this._parent.children.indexOf(this); 1.294 + if (index != -1) { 1.295 + this._parent.children.splice(index, 1); 1.296 + } 1.297 + } 1.298 + this._parent = parent; 1.299 + parent.children.push(this); 1.300 + this.updateAppenders(); 1.301 + }, 1.302 + 1.303 + updateAppenders: function updateAppenders() { 1.304 + if (this._parent) { 1.305 + let notOwnAppenders = this._parent.appenders.filter(function(appender) { 1.306 + return this.ownAppenders.indexOf(appender) == -1; 1.307 + }, this); 1.308 + this.appenders = notOwnAppenders.concat(this.ownAppenders); 1.309 + } else { 1.310 + this.appenders = this.ownAppenders.slice(); 1.311 + } 1.312 + 1.313 + // Update children's appenders. 1.314 + for (let i = 0; i < this.children.length; i++) { 1.315 + this.children[i].updateAppenders(); 1.316 + } 1.317 + }, 1.318 + 1.319 + addAppender: function Logger_addAppender(appender) { 1.320 + if (this.ownAppenders.indexOf(appender) != -1) { 1.321 + return; 1.322 + } 1.323 + this.ownAppenders.push(appender); 1.324 + this.updateAppenders(); 1.325 + }, 1.326 + 1.327 + removeAppender: function Logger_removeAppender(appender) { 1.328 + let index = this.ownAppenders.indexOf(appender); 1.329 + if (index == -1) { 1.330 + return; 1.331 + } 1.332 + this.ownAppenders.splice(index, 1); 1.333 + this.updateAppenders(); 1.334 + }, 1.335 + 1.336 + /** 1.337 + * Logs a structured message object. 1.338 + * 1.339 + * @param action 1.340 + * (string) A message action, one of a set of actions known to the 1.341 + * log consumer. 1.342 + * @param params 1.343 + * (object) Parameters to be included in the message. 1.344 + * If _level is included as a key and the corresponding value 1.345 + * is a number or known level name, the message will be logged 1.346 + * at the indicated level. If _message is included as a key, the 1.347 + * value is used as the descriptive text for the message. 1.348 + */ 1.349 + logStructured: function (action, params) { 1.350 + if (!action) { 1.351 + throw "An action is required when logging a structured message."; 1.352 + } 1.353 + if (!params) { 1.354 + return this.log(this.level, undefined, {"action": action}); 1.355 + } 1.356 + if (typeof(params) != "object") { 1.357 + throw "The params argument is required to be an object."; 1.358 + } 1.359 + 1.360 + let level = params._level; 1.361 + if (level) { 1.362 + let ulevel = level.toUpperCase(); 1.363 + if (ulevel in Log.Level.Numbers) { 1.364 + level = Log.Level.Numbers[ulevel]; 1.365 + } 1.366 + } else { 1.367 + level = this.level; 1.368 + } 1.369 + 1.370 + params.action = action; 1.371 + this.log(level, params._message, params); 1.372 + }, 1.373 + 1.374 + log: function (level, string, params) { 1.375 + if (this.level > level) 1.376 + return; 1.377 + 1.378 + // Hold off on creating the message object until we actually have 1.379 + // an appender that's responsible. 1.380 + let message; 1.381 + let appenders = this.appenders; 1.382 + for (let appender of appenders) { 1.383 + if (appender.level > level) { 1.384 + continue; 1.385 + } 1.386 + if (!message) { 1.387 + message = new LogMessage(this._name, level, string, params); 1.388 + } 1.389 + appender.append(message); 1.390 + } 1.391 + }, 1.392 + 1.393 + fatal: function (string, params) { 1.394 + this.log(Log.Level.Fatal, string, params); 1.395 + }, 1.396 + error: function (string, params) { 1.397 + this.log(Log.Level.Error, string, params); 1.398 + }, 1.399 + warn: function (string, params) { 1.400 + this.log(Log.Level.Warn, string, params); 1.401 + }, 1.402 + info: function (string, params) { 1.403 + this.log(Log.Level.Info, string, params); 1.404 + }, 1.405 + config: function (string, params) { 1.406 + this.log(Log.Level.Config, string, params); 1.407 + }, 1.408 + debug: function (string, params) { 1.409 + this.log(Log.Level.Debug, string, params); 1.410 + }, 1.411 + trace: function (string, params) { 1.412 + this.log(Log.Level.Trace, string, params); 1.413 + } 1.414 +}; 1.415 + 1.416 +/* 1.417 + * LoggerRepository 1.418 + * Implements a hierarchy of Loggers 1.419 + */ 1.420 + 1.421 +function LoggerRepository() {} 1.422 +LoggerRepository.prototype = { 1.423 + _loggers: {}, 1.424 + 1.425 + _rootLogger: null, 1.426 + get rootLogger() { 1.427 + if (!this._rootLogger) { 1.428 + this._rootLogger = new Logger("root", this); 1.429 + this._rootLogger.level = Log.Level.All; 1.430 + } 1.431 + return this._rootLogger; 1.432 + }, 1.433 + set rootLogger(logger) { 1.434 + throw "Cannot change the root logger"; 1.435 + }, 1.436 + 1.437 + _updateParents: function LogRep__updateParents(name) { 1.438 + let pieces = name.split('.'); 1.439 + let cur, parent; 1.440 + 1.441 + // find the closest parent 1.442 + // don't test for the logger name itself, as there's a chance it's already 1.443 + // there in this._loggers 1.444 + for (let i = 0; i < pieces.length - 1; i++) { 1.445 + if (cur) 1.446 + cur += '.' + pieces[i]; 1.447 + else 1.448 + cur = pieces[i]; 1.449 + if (cur in this._loggers) 1.450 + parent = cur; 1.451 + } 1.452 + 1.453 + // if we didn't assign a parent above, there is no parent 1.454 + if (!parent) 1.455 + this._loggers[name].parent = this.rootLogger; 1.456 + else 1.457 + this._loggers[name].parent = this._loggers[parent]; 1.458 + 1.459 + // trigger updates for any possible descendants of this logger 1.460 + for (let logger in this._loggers) { 1.461 + if (logger != name && logger.indexOf(name) == 0) 1.462 + this._updateParents(logger); 1.463 + } 1.464 + }, 1.465 + 1.466 + /** 1.467 + * Obtain a named Logger. 1.468 + * 1.469 + * The returned Logger instance for a particular name is shared among 1.470 + * all callers. In other words, if two consumers call getLogger("foo"), 1.471 + * they will both have a reference to the same object. 1.472 + * 1.473 + * @return Logger 1.474 + */ 1.475 + getLogger: function (name) { 1.476 + if (name in this._loggers) 1.477 + return this._loggers[name]; 1.478 + this._loggers[name] = new Logger(name, this); 1.479 + this._updateParents(name); 1.480 + return this._loggers[name]; 1.481 + }, 1.482 + 1.483 + /** 1.484 + * Obtain a Logger that logs all string messages with a prefix. 1.485 + * 1.486 + * A common pattern is to have separate Logger instances for each instance 1.487 + * of an object. But, you still want to distinguish between each instance. 1.488 + * Since Log.repository.getLogger() returns shared Logger objects, 1.489 + * monkeypatching one Logger modifies them all. 1.490 + * 1.491 + * This function returns a new object with a prototype chain that chains 1.492 + * up to the original Logger instance. The new prototype has log functions 1.493 + * that prefix content to each message. 1.494 + * 1.495 + * @param name 1.496 + * (string) The Logger to retrieve. 1.497 + * @param prefix 1.498 + * (string) The string to prefix each logged message with. 1.499 + */ 1.500 + getLoggerWithMessagePrefix: function (name, prefix) { 1.501 + let log = this.getLogger(name); 1.502 + 1.503 + let proxy = {__proto__: log}; 1.504 + 1.505 + for (let level in Log.Level) { 1.506 + if (level == "Desc") { 1.507 + continue; 1.508 + } 1.509 + 1.510 + let lc = level.toLowerCase(); 1.511 + proxy[lc] = function (msg, ...args) { 1.512 + return log[lc].apply(log, [prefix + msg, ...args]); 1.513 + }; 1.514 + } 1.515 + 1.516 + return proxy; 1.517 + }, 1.518 +}; 1.519 + 1.520 +/* 1.521 + * Formatters 1.522 + * These massage a LogMessage into whatever output is desired. 1.523 + * BasicFormatter and StructuredFormatter are implemented here. 1.524 + */ 1.525 + 1.526 +// Abstract formatter 1.527 +function Formatter() {} 1.528 +Formatter.prototype = { 1.529 + format: function Formatter_format(message) {} 1.530 +}; 1.531 + 1.532 +// Basic formatter that doesn't do anything fancy. 1.533 +function BasicFormatter(dateFormat) { 1.534 + if (dateFormat) { 1.535 + this.dateFormat = dateFormat; 1.536 + } 1.537 + this.parameterFormatter = new ParameterFormatter(); 1.538 +} 1.539 +BasicFormatter.prototype = { 1.540 + __proto__: Formatter.prototype, 1.541 + 1.542 + /** 1.543 + * Format the text of a message with optional parameters. 1.544 + * If the text contains ${identifier}, replace that with 1.545 + * the value of params[identifier]; if ${}, replace that with 1.546 + * the entire params object. If no params have been substituted 1.547 + * into the text, format the entire object and append that 1.548 + * to the message. 1.549 + */ 1.550 + formatText: function (message) { 1.551 + let params = message.params; 1.552 + if (!params) { 1.553 + return message.message || ""; 1.554 + } 1.555 + // Defensive handling of non-object params 1.556 + // We could add a special case for NSRESULT values here... 1.557 + let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function'); 1.558 + 1.559 + // if we have params, try and find substitutions. 1.560 + if (message.params && this.parameterFormatter) { 1.561 + // have we successfully substituted any parameters into the message? 1.562 + // in the log message 1.563 + let subDone = false; 1.564 + let regex = /\$\{(\S*)\}/g; 1.565 + let textParts = []; 1.566 + if (message.message) { 1.567 + textParts.push(message.message.replace(regex, (_, sub) => { 1.568 + // ${foo} means use the params['foo'] 1.569 + if (sub) { 1.570 + if (pIsObject && sub in message.params) { 1.571 + subDone = true; 1.572 + return this.parameterFormatter.format(message.params[sub]); 1.573 + } 1.574 + return '${' + sub + '}'; 1.575 + } 1.576 + // ${} means use the entire params object. 1.577 + subDone = true; 1.578 + return this.parameterFormatter.format(message.params); 1.579 + })); 1.580 + } 1.581 + if (!subDone) { 1.582 + // There were no substitutions in the text, so format the entire params object 1.583 + let rest = this.parameterFormatter.format(message.params); 1.584 + if (rest !== null && rest != "{}") { 1.585 + textParts.push(rest); 1.586 + } 1.587 + } 1.588 + return textParts.join(': '); 1.589 + } 1.590 + }, 1.591 + 1.592 + format: function BF_format(message) { 1.593 + return message.time + "\t" + 1.594 + message.loggerName + "\t" + 1.595 + message.levelDesc + "\t" + 1.596 + this.formatText(message); 1.597 + } 1.598 +}; 1.599 + 1.600 +/** 1.601 + * A formatter that only formats the string message component. 1.602 + */ 1.603 +function MessageOnlyFormatter() { 1.604 +} 1.605 +MessageOnlyFormatter.prototype = Object.freeze({ 1.606 + __proto__: Formatter.prototype, 1.607 + 1.608 + format: function (message) { 1.609 + return message.message; 1.610 + }, 1.611 +}); 1.612 + 1.613 +// Structured formatter that outputs JSON based on message data. 1.614 +// This formatter will format unstructured messages by supplying 1.615 +// default values. 1.616 +function StructuredFormatter() { } 1.617 +StructuredFormatter.prototype = { 1.618 + __proto__: Formatter.prototype, 1.619 + 1.620 + format: function (logMessage) { 1.621 + let output = { 1.622 + _time: logMessage.time, 1.623 + _namespace: logMessage.loggerName, 1.624 + _level: logMessage.levelDesc 1.625 + }; 1.626 + 1.627 + for (let key in logMessage.params) { 1.628 + output[key] = logMessage.params[key]; 1.629 + } 1.630 + 1.631 + if (!output.action) { 1.632 + output.action = "UNKNOWN"; 1.633 + } 1.634 + 1.635 + if (!output._message && logMessage.message) { 1.636 + output._message = logMessage.message; 1.637 + } 1.638 + 1.639 + return JSON.stringify(output); 1.640 + } 1.641 +} 1.642 + 1.643 +/** 1.644 + * Test an object to see if it is a Mozilla JS Error. 1.645 + */ 1.646 +function isError(aObj) { 1.647 + return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj && 1.648 + "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj); 1.649 +}; 1.650 + 1.651 +/* 1.652 + * Parameter Formatters 1.653 + * These massage an object used as a parameter for a LogMessage into 1.654 + * a string representation of the object. 1.655 + */ 1.656 + 1.657 +function ParameterFormatter() { 1.658 + this._name = "ParameterFormatter" 1.659 +} 1.660 +ParameterFormatter.prototype = { 1.661 + format: function(ob) { 1.662 + try { 1.663 + if (ob === undefined) { 1.664 + return "undefined"; 1.665 + } 1.666 + if (ob === null) { 1.667 + return "null"; 1.668 + } 1.669 + // Pass through primitive types and objects that unbox to primitive types. 1.670 + if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") && 1.671 + typeof(ob) != "function") { 1.672 + return ob; 1.673 + } 1.674 + if (ob instanceof Ci.nsIException) { 1.675 + return ob.toString() + " " + Log.stackTrace(ob); 1.676 + } 1.677 + else if (isError(ob)) { 1.678 + return Log._formatError(ob); 1.679 + } 1.680 + // Just JSONify it. Filter out our internal fields and those the caller has 1.681 + // already handled. 1.682 + return JSON.stringify(ob, (key, val) => { 1.683 + if (INTERNAL_FIELDS.has(key)) { 1.684 + return undefined; 1.685 + } 1.686 + return val; 1.687 + }); 1.688 + } 1.689 + catch (e) { 1.690 + dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e)); 1.691 + } 1.692 + // Fancy formatting failed. Just toSource() it - but even this may fail! 1.693 + try { 1.694 + return ob.toSource(); 1.695 + } catch (_) { } 1.696 + try { 1.697 + return "" + ob; 1.698 + } catch (_) { 1.699 + return "[object]" 1.700 + } 1.701 + } 1.702 +} 1.703 + 1.704 +/* 1.705 + * Appenders 1.706 + * These can be attached to Loggers to log to different places 1.707 + * Simply subclass and override doAppend to implement a new one 1.708 + */ 1.709 + 1.710 +function Appender(formatter) { 1.711 + this._name = "Appender"; 1.712 + this._formatter = formatter? formatter : new BasicFormatter(); 1.713 +} 1.714 +Appender.prototype = { 1.715 + level: Log.Level.All, 1.716 + 1.717 + append: function App_append(message) { 1.718 + if (message) { 1.719 + this.doAppend(this._formatter.format(message)); 1.720 + } 1.721 + }, 1.722 + toString: function App_toString() { 1.723 + return this._name + " [level=" + this.level + 1.724 + ", formatter=" + this._formatter + "]"; 1.725 + }, 1.726 + doAppend: function App_doAppend(formatted) {} 1.727 +}; 1.728 + 1.729 +/* 1.730 + * DumpAppender 1.731 + * Logs to standard out 1.732 + */ 1.733 + 1.734 +function DumpAppender(formatter) { 1.735 + Appender.call(this, formatter); 1.736 + this._name = "DumpAppender"; 1.737 +} 1.738 +DumpAppender.prototype = { 1.739 + __proto__: Appender.prototype, 1.740 + 1.741 + doAppend: function DApp_doAppend(formatted) { 1.742 + dump(formatted + "\n"); 1.743 + } 1.744 +}; 1.745 + 1.746 +/* 1.747 + * ConsoleAppender 1.748 + * Logs to the javascript console 1.749 + */ 1.750 + 1.751 +function ConsoleAppender(formatter) { 1.752 + Appender.call(this, formatter); 1.753 + this._name = "ConsoleAppender"; 1.754 +} 1.755 +ConsoleAppender.prototype = { 1.756 + __proto__: Appender.prototype, 1.757 + 1.758 + // XXX this should be replaced with calls to the Browser Console 1.759 + append: function App_append(message) { 1.760 + if (message) { 1.761 + let m = this._formatter.format(message); 1.762 + if (message.level > Log.Level.Warn) { 1.763 + Cu.reportError(m); 1.764 + return; 1.765 + } 1.766 + this.doAppend(m); 1.767 + } 1.768 + }, 1.769 + 1.770 + doAppend: function CApp_doAppend(formatted) { 1.771 + Cc["@mozilla.org/consoleservice;1"]. 1.772 + getService(Ci.nsIConsoleService).logStringMessage(formatted); 1.773 + } 1.774 +}; 1.775 + 1.776 +/** 1.777 + * Append to an nsIStorageStream 1.778 + * 1.779 + * This writes logging output to an in-memory stream which can later be read 1.780 + * back as an nsIInputStream. It can be used to avoid expensive I/O operations 1.781 + * during logging. Instead, one can periodically consume the input stream and 1.782 + * e.g. write it to disk asynchronously. 1.783 + */ 1.784 +function StorageStreamAppender(formatter) { 1.785 + Appender.call(this, formatter); 1.786 + this._name = "StorageStreamAppender"; 1.787 +} 1.788 + 1.789 +StorageStreamAppender.prototype = { 1.790 + __proto__: Appender.prototype, 1.791 + 1.792 + _converterStream: null, // holds the nsIConverterOutputStream 1.793 + _outputStream: null, // holds the underlying nsIOutputStream 1.794 + 1.795 + _ss: null, 1.796 + 1.797 + get outputStream() { 1.798 + if (!this._outputStream) { 1.799 + // First create a raw stream. We can bail out early if that fails. 1.800 + this._outputStream = this.newOutputStream(); 1.801 + if (!this._outputStream) { 1.802 + return null; 1.803 + } 1.804 + 1.805 + // Wrap the raw stream in an nsIConverterOutputStream. We can reuse 1.806 + // the instance if we already have one. 1.807 + if (!this._converterStream) { 1.808 + this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"] 1.809 + .createInstance(Ci.nsIConverterOutputStream); 1.810 + } 1.811 + this._converterStream.init( 1.812 + this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE, 1.813 + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 1.814 + } 1.815 + return this._converterStream; 1.816 + }, 1.817 + 1.818 + newOutputStream: function newOutputStream() { 1.819 + let ss = this._ss = Cc["@mozilla.org/storagestream;1"] 1.820 + .createInstance(Ci.nsIStorageStream); 1.821 + ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null); 1.822 + return ss.getOutputStream(0); 1.823 + }, 1.824 + 1.825 + getInputStream: function getInputStream() { 1.826 + if (!this._ss) { 1.827 + return null; 1.828 + } 1.829 + return this._ss.newInputStream(0); 1.830 + }, 1.831 + 1.832 + reset: function reset() { 1.833 + if (!this._outputStream) { 1.834 + return; 1.835 + } 1.836 + this.outputStream.close(); 1.837 + this._outputStream = null; 1.838 + this._ss = null; 1.839 + }, 1.840 + 1.841 + doAppend: function (formatted) { 1.842 + if (!formatted) { 1.843 + return; 1.844 + } 1.845 + try { 1.846 + this.outputStream.writeString(formatted + "\n"); 1.847 + } catch(ex) { 1.848 + if (ex.result == Cr.NS_BASE_STREAM_CLOSED) { 1.849 + // The underlying output stream is closed, so let's open a new one 1.850 + // and try again. 1.851 + this._outputStream = null; 1.852 + } try { 1.853 + this.outputStream.writeString(formatted + "\n"); 1.854 + } catch (ex) { 1.855 + // Ah well, we tried, but something seems to be hosed permanently. 1.856 + } 1.857 + } 1.858 + } 1.859 +}; 1.860 + 1.861 +/** 1.862 + * File appender 1.863 + * 1.864 + * Writes output to file using OS.File. 1.865 + */ 1.866 +function FileAppender(path, formatter) { 1.867 + Appender.call(this, formatter); 1.868 + this._name = "FileAppender"; 1.869 + this._encoder = new TextEncoder(); 1.870 + this._path = path; 1.871 + this._file = null; 1.872 + this._fileReadyPromise = null; 1.873 + 1.874 + // This is a promise exposed for testing/debugging the logger itself. 1.875 + this._lastWritePromise = null; 1.876 +} 1.877 + 1.878 +FileAppender.prototype = { 1.879 + __proto__: Appender.prototype, 1.880 + 1.881 + _openFile: function () { 1.882 + return Task.spawn(function _openFile() { 1.883 + try { 1.884 + this._file = yield OS.File.open(this._path, 1.885 + {truncate: true}); 1.886 + } catch (err) { 1.887 + if (err instanceof OS.File.Error) { 1.888 + this._file = null; 1.889 + } else { 1.890 + throw err; 1.891 + } 1.892 + } 1.893 + }.bind(this)); 1.894 + }, 1.895 + 1.896 + _getFile: function() { 1.897 + if (!this._fileReadyPromise) { 1.898 + this._fileReadyPromise = this._openFile(); 1.899 + return this._fileReadyPromise; 1.900 + } 1.901 + 1.902 + return this._fileReadyPromise.then(_ => { 1.903 + if (!this._file) { 1.904 + return this._openFile(); 1.905 + } 1.906 + }); 1.907 + }, 1.908 + 1.909 + doAppend: function (formatted) { 1.910 + let array = this._encoder.encode(formatted + "\n"); 1.911 + if (this._file) { 1.912 + this._lastWritePromise = this._file.write(array); 1.913 + } else { 1.914 + this._lastWritePromise = this._getFile().then(_ => { 1.915 + this._fileReadyPromise = null; 1.916 + if (this._file) { 1.917 + return this._file.write(array); 1.918 + } 1.919 + }); 1.920 + } 1.921 + }, 1.922 + 1.923 + reset: function () { 1.924 + let fileClosePromise = this._file.close(); 1.925 + return fileClosePromise.then(_ => { 1.926 + this._file = null; 1.927 + return OS.File.remove(this._path); 1.928 + }); 1.929 + } 1.930 +}; 1.931 + 1.932 +/** 1.933 + * Bounded File appender 1.934 + * 1.935 + * Writes output to file using OS.File. After the total message size 1.936 + * (as defined by formatted.length) exceeds maxSize, existing messages 1.937 + * will be discarded, and subsequent writes will be appended to a new log file. 1.938 + */ 1.939 +function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) { 1.940 + FileAppender.call(this, path, formatter); 1.941 + this._name = "BoundedFileAppender"; 1.942 + this._size = 0; 1.943 + this._maxSize = maxSize; 1.944 + this._closeFilePromise = null; 1.945 +} 1.946 + 1.947 +BoundedFileAppender.prototype = { 1.948 + __proto__: FileAppender.prototype, 1.949 + 1.950 + doAppend: function (formatted) { 1.951 + if (!this._removeFilePromise) { 1.952 + if (this._size < this._maxSize) { 1.953 + this._size += formatted.length; 1.954 + return FileAppender.prototype.doAppend.call(this, formatted); 1.955 + } 1.956 + this._removeFilePromise = this.reset(); 1.957 + } 1.958 + this._removeFilePromise.then(_ => { 1.959 + this._removeFilePromise = null; 1.960 + this.doAppend(formatted); 1.961 + }); 1.962 + }, 1.963 + 1.964 + reset: function () { 1.965 + let fileClosePromise; 1.966 + if (this._fileReadyPromise) { 1.967 + // An attempt to open the file may still be in progress. 1.968 + fileClosePromise = this._fileReadyPromise.then(_ => { 1.969 + return this._file.close(); 1.970 + }); 1.971 + } else { 1.972 + fileClosePromise = this._file.close(); 1.973 + } 1.974 + 1.975 + return fileClosePromise.then(_ => { 1.976 + this._size = 0; 1.977 + this._file = null; 1.978 + return OS.File.remove(this._path); 1.979 + }); 1.980 + } 1.981 +}; 1.982 +