toolkit/modules/Log.jsm

changeset 0
6474c204b198
     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 +

mercurial