1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["Log"];
9 const { XPCOMUtils } = ChromeUtils.import(
10 "resource://gre/modules/XPCOMUtils.jsm"
12 ChromeUtils.defineModuleGetter(
15 "resource://gre/modules/Services.jsm"
17 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
20 * Dump a message everywhere we can if we have a failure.
22 function dumpError(text) {
36 All: -1, // We don't want All to be falsy.
60 delete Log.repository;
61 Log.repository = new LoggerRepository();
62 return Log.repository;
64 set repository(value) {
65 delete Log.repository;
66 Log.repository = value;
70 let result = String(e);
72 let loc = [e.fileName];
74 loc.push(e.lineNumber);
77 loc.push(e.columnNumber);
79 result += `(${loc.join(":")})`;
81 return `${result} ${Log.stackTrace(e)}`;
84 // This is for back compatibility with services/common/utils.js; we duplicate
85 // some of the logic in ParameterFormatter
90 if (e instanceof Ci.nsIException) {
91 return `${e} ${Log.stackTrace(e)}`;
92 } else if (isError(e)) {
93 return Log._formatError(e);
96 let message = e.message || e;
97 return `${message} ${Log.stackTrace(e)}`;
101 // Wrapped nsIException
103 let frame = e.location;
106 // Works on frames or exceptions, munges file:// URIs to shorten the paths
107 // FIXME: filename munging is sort of hackish, might be confusing if
108 // there are multiple extensions with similar filenames
109 let str = "<file:unknown>";
111 let file = frame.filename || frame.fileName;
113 str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
116 if (frame.lineNumber) {
117 str += ":" + frame.lineNumber;
121 str = frame.name + "()@" + str;
127 frame = frame.caller;
129 return `Stack trace: ${output.join("\n")}`;
131 // Standard JS exception
136 stack.trim().replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1")
140 return "No traceback available";
146 * Encapsulates a single log event's data
149 constructor(loggerName, level, message, params) {
150 this.loggerName = loggerName;
153 * Special case to handle "log./level/(object)", for example logging a caught exception
154 * without providing text or params like: catch(e) { logger.warn(e) }
155 * Treating this as an empty text with the object in the 'params' field causes the
156 * object to be formatted properly by BasicFormatter.
161 typeof message == "object" &&
162 typeof message.valueOf() != "string"
165 this.params = message;
167 // If the message text is empty, or a string, or a String object, normal handling
168 this.message = message;
169 this.params = params;
172 // The _structured field will correspond to whether this message is to
173 // be interpreted as a structured message.
174 this._structured = this.params && this.params.action;
175 this.time = Date.now();
179 if (this.level in Log.Level.Desc) {
180 return Log.Level.Desc[this.level];
186 let msg = `${this.time} ${this.level} ${this.message}`;
188 msg += ` ${JSON.stringify(this.params)}`;
190 return `LogMessage [${msg}]`;
196 * Hierarchical version. Logs to all appenders, assigned or inherited
200 constructor(name, repository) {
202 repository = Log.repository;
206 this.ownAppenders = [];
208 this._repository = repository;
210 this._levelPrefName = null;
211 this._levelPrefValue = null;
221 if (this._levelPrefName) {
222 // We've been asked to use a preference to configure the logs. If the
223 // pref has a value we use it, otherwise we continue to use the parent.
224 const lpv = this._levelPrefValue;
226 const levelValue = Log.Level[lpv];
228 // stash it in _level just in case a future value of the pref is
229 // invalid, in which case we end up continuing to use this value.
230 this._level = levelValue;
234 // in case the pref has transitioned from a value to no value, we reset
235 // this._level and fall through to using the parent.
239 if (this._level != null) {
243 return this.parent.level;
245 dumpError("Log warning: root logger configuration error: no level defined");
246 return Log.Level.All;
249 if (this._levelPrefName) {
250 // I guess we could honor this by nuking this._levelPrefValue, but it
251 // almost certainly implies confusion, so we'll warn and ignore.
253 `Log warning: The log '${this.name}' is configured to use ` +
254 `the preference '${this._levelPrefName}' - you must adjust ` +
255 `the level by setting this preference, not by using the ` +
267 if (this._parent == parent) {
270 // Remove ourselves from parent's children
272 let index = this._parent.children.indexOf(this);
274 this._parent.children.splice(index, 1);
277 this._parent = parent;
278 parent.children.push(this);
279 this.updateAppenders();
282 manageLevelFromPref(prefName) {
283 if (prefName == this._levelPrefName) {
284 // We've already configured this log with an observer for that pref.
287 if (this._levelPrefName) {
289 `The log '${this.name}' is already configured with the ` +
290 `preference '${this._levelPrefName}' - ignoring request to ` +
291 `also use the preference '${prefName}'`
295 this._levelPrefName = prefName;
296 XPCOMUtils.defineLazyPreferenceGetter(this, "_levelPrefValue", prefName);
301 let notOwnAppenders = this._parent.appenders.filter(function(appender) {
302 return !this.ownAppenders.includes(appender);
304 this.appenders = notOwnAppenders.concat(this.ownAppenders);
306 this.appenders = this.ownAppenders.slice();
309 // Update children's appenders.
310 for (let i = 0; i < this.children.length; i++) {
311 this.children[i].updateAppenders();
315 addAppender(appender) {
316 if (this.ownAppenders.includes(appender)) {
319 this.ownAppenders.push(appender);
320 this.updateAppenders();
323 removeAppender(appender) {
324 let index = this.ownAppenders.indexOf(appender);
328 this.ownAppenders.splice(index, 1);
329 this.updateAppenders();
332 _unpackTemplateLiteral(string, params) {
333 if (!Array.isArray(params)) {
334 // Regular log() call.
335 return [string, params];
338 if (!Array.isArray(string)) {
339 // Not using template literal. However params was packed into an array by
340 // the this.[level] call, so we need to unpack it here.
341 return [string, params[0]];
344 // We're using template literal format (logger.warn `foo ${bar}`). Turn the
345 // template strings into one string containing "${0}"..."${n}" tokens, and
346 // feed it to the basic formatter. The formatter will treat the numbers as
347 // indices into the params array, and convert the tokens to the params.
349 if (!params.length) {
350 // No params; we need to set params to undefined, so the formatter
351 // doesn't try to output the params array.
352 return [string[0], undefined];
355 let concat = string[0];
356 for (let i = 0; i < params.length; i++) {
357 concat += `\${${i}}${string[i + 1]}`;
359 return [concat, params];
362 log(level, string, params) {
363 if (this.level > level) {
367 // Hold off on creating the message object until we actually have
368 // an appender that's responsible.
370 let appenders = this.appenders;
371 for (let appender of appenders) {
372 if (appender.level > level) {
376 [string, params] = this._unpackTemplateLiteral(string, params);
377 message = new LogMessage(this._name, level, string, params);
379 appender.append(message);
383 fatal(string, ...params) {
384 this.log(Log.Level.Fatal, string, params);
386 error(string, ...params) {
387 this.log(Log.Level.Error, string, params);
389 warn(string, ...params) {
390 this.log(Log.Level.Warn, string, params);
392 info(string, ...params) {
393 this.log(Log.Level.Info, string, params);
395 config(string, ...params) {
396 this.log(Log.Level.Config, string, params);
398 debug(string, ...params) {
399 this.log(Log.Level.Debug, string, params);
401 trace(string, ...params) {
402 this.log(Log.Level.Trace, string, params);
408 * Implements a hierarchy of Loggers
411 class LoggerRepository {
414 this._rootLogger = null;
418 if (!this._rootLogger) {
419 this._rootLogger = new Logger("root", this);
420 this._rootLogger.level = Log.Level.All;
422 return this._rootLogger;
424 set rootLogger(logger) {
425 throw new Error("Cannot change the root logger");
428 _updateParents(name) {
429 let pieces = name.split(".");
432 // find the closest parent
433 // don't test for the logger name itself, as there's a chance it's already
434 // there in this._loggers
435 for (let i = 0; i < pieces.length - 1; i++) {
437 cur += "." + pieces[i];
441 if (cur in this._loggers) {
446 // if we didn't assign a parent above, there is no parent
448 this._loggers[name].parent = this.rootLogger;
450 this._loggers[name].parent = this._loggers[parent];
453 // trigger updates for any possible descendants of this logger
454 for (let logger in this._loggers) {
455 if (logger != name && logger.indexOf(name) == 0) {
456 this._updateParents(logger);
462 * Obtain a named Logger.
464 * The returned Logger instance for a particular name is shared among
465 * all callers. In other words, if two consumers call getLogger("foo"),
466 * they will both have a reference to the same object.
471 if (name in this._loggers) {
472 return this._loggers[name];
474 this._loggers[name] = new Logger(name, this);
475 this._updateParents(name);
476 return this._loggers[name];
480 * Obtain a Logger that logs all string messages with a prefix.
482 * A common pattern is to have separate Logger instances for each instance
483 * of an object. But, you still want to distinguish between each instance.
484 * Since Log.repository.getLogger() returns shared Logger objects,
485 * monkeypatching one Logger modifies them all.
487 * This function returns a new object with a prototype chain that chains
488 * up to the original Logger instance. The new prototype has log functions
489 * that prefix content to each message.
492 * (string) The Logger to retrieve.
494 * (string) The string to prefix each logged message with.
496 getLoggerWithMessagePrefix(name, prefix) {
497 let log = this.getLogger(name);
499 let proxy = Object.create(log);
500 proxy.log = (level, string, params) => {
501 if (Array.isArray(string) && Array.isArray(params)) {
503 // We cannot change the original array, so create a new one.
504 string = [prefix + string[0]].concat(string.slice(1));
506 string = prefix + string; // Regular string.
508 return log.log(level, string, params);
516 * These massage a LogMessage into whatever output is desired.
519 // Basic formatter that doesn't do anything fancy.
520 class BasicFormatter {
521 constructor(dateFormat) {
523 this.dateFormat = dateFormat;
525 this.parameterFormatter = new ParameterFormatter();
529 * Format the text of a message with optional parameters.
530 * If the text contains ${identifier}, replace that with
531 * the value of params[identifier]; if ${}, replace that with
532 * the entire params object. If no params have been substituted
533 * into the text, format the entire object and append that
536 formatText(message) {
537 let params = message.params;
538 if (typeof params == "undefined") {
539 return message.message || "";
541 // Defensive handling of non-object params
542 // We could add a special case for NSRESULT values here...
543 let pIsObject = typeof params == "object" || typeof params == "function";
545 // if we have params, try and find substitutions.
546 if (this.parameterFormatter) {
547 // have we successfully substituted any parameters into the message?
548 // in the log message
550 let regex = /\$\{(\S*?)\}/g;
552 if (message.message) {
554 message.message.replace(regex, (_, sub) => {
555 // ${foo} means use the params['foo']
557 if (pIsObject && sub in message.params) {
559 return this.parameterFormatter.format(message.params[sub]);
561 return "${" + sub + "}";
563 // ${} means use the entire params object.
565 return this.parameterFormatter.format(message.params);
570 // There were no substitutions in the text, so format the entire params object
571 let rest = this.parameterFormatter.format(message.params);
572 if (rest !== null && rest != "{}") {
573 textParts.push(rest);
576 return textParts.join(": ");
589 this.formatText(message)
595 * Test an object to see if it is a Mozilla JS Error.
597 function isError(aObj) {
600 typeof aObj == "object" &&
603 "fileName" in aObj &&
604 "lineNumber" in aObj &&
610 * Parameter Formatters
611 * These massage an object used as a parameter for a LogMessage into
612 * a string representation of the object.
615 class ParameterFormatter {
617 this._name = "ParameterFormatter";
622 if (ob === undefined) {
628 // Pass through primitive types and objects that unbox to primitive types.
630 (typeof ob != "object" || typeof ob.valueOf() != "object") &&
631 typeof ob != "function"
635 if (ob instanceof Ci.nsIException) {
636 return `${ob} ${Log.stackTrace(ob)}`;
637 } else if (isError(ob)) {
638 return Log._formatError(ob);
640 // Just JSONify it. Filter out our internal fields and those the caller has
642 return JSON.stringify(ob, (key, val) => {
643 if (INTERNAL_FIELDS.has(key)) {
650 `Exception trying to format object for log message: ${Log.exceptionStr(
655 // Fancy formatting failed. Just toSource() it - but even this may fail!
657 return ob.toSource();
669 * These can be attached to Loggers to log to different places
670 * Simply subclass and override doAppend to implement a new one
674 constructor(formatter) {
675 this.level = Log.Level.All;
676 this._name = "Appender";
677 this._formatter = formatter || new BasicFormatter();
682 this.doAppend(this._formatter.format(message));
687 return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
693 * Logs to standard out
696 class DumpAppender extends Appender {
697 constructor(formatter) {
699 this._name = "DumpAppender";
702 doAppend(formatted) {
703 dump(formatted + "\n");
709 * Logs to the javascript console
712 class ConsoleAppender extends Appender {
713 constructor(formatter) {
715 this._name = "ConsoleAppender";
718 // XXX this should be replaced with calls to the Browser Console
721 let m = this._formatter.format(message);
722 if (message.level > Log.Level.Warn) {
730 doAppend(formatted) {
731 Services.console.logStringMessage(formatted);