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/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["Log"];
8
9const { XPCOMUtils } = ChromeUtils.import(
10  "resource://gre/modules/XPCOMUtils.jsm"
11);
12ChromeUtils.defineModuleGetter(
13  this,
14  "Services",
15  "resource://gre/modules/Services.jsm"
16);
17const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
18
19/*
20 * Dump a message everywhere we can if we have a failure.
21 */
22function dumpError(text) {
23  dump(text + "\n");
24  Cu.reportError(text);
25}
26
27var Log = {
28  Level: {
29    Fatal: 70,
30    Error: 60,
31    Warn: 50,
32    Info: 40,
33    Config: 30,
34    Debug: 20,
35    Trace: 10,
36    All: -1, // We don't want All to be falsy.
37    Desc: {
38      70: "FATAL",
39      60: "ERROR",
40      50: "WARN",
41      40: "INFO",
42      30: "CONFIG",
43      20: "DEBUG",
44      10: "TRACE",
45      "-1": "ALL",
46    },
47    Numbers: {
48      FATAL: 70,
49      ERROR: 60,
50      WARN: 50,
51      INFO: 40,
52      CONFIG: 30,
53      DEBUG: 20,
54      TRACE: 10,
55      ALL: -1,
56    },
57  },
58
59  get repository() {
60    delete Log.repository;
61    Log.repository = new LoggerRepository();
62    return Log.repository;
63  },
64  set repository(value) {
65    delete Log.repository;
66    Log.repository = value;
67  },
68
69  _formatError(e) {
70    let result = String(e);
71    if (e.fileName) {
72      let loc = [e.fileName];
73      if (e.lineNumber) {
74        loc.push(e.lineNumber);
75      }
76      if (e.columnNumber) {
77        loc.push(e.columnNumber);
78      }
79      result += `(${loc.join(":")})`;
80    }
81    return `${result} ${Log.stackTrace(e)}`;
82  },
83
84  // This is for back compatibility with services/common/utils.js; we duplicate
85  // some of the logic in ParameterFormatter
86  exceptionStr(e) {
87    if (!e) {
88      return String(e);
89    }
90    if (e instanceof Ci.nsIException) {
91      return `${e} ${Log.stackTrace(e)}`;
92    } else if (isError(e)) {
93      return Log._formatError(e);
94    }
95    // else
96    let message = e.message || e;
97    return `${message} ${Log.stackTrace(e)}`;
98  },
99
100  stackTrace(e) {
101    // Wrapped nsIException
102    if (e.location) {
103      let frame = e.location;
104      let output = [];
105      while (frame) {
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>";
110
111        let file = frame.filename || frame.fileName;
112        if (file) {
113          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
114        }
115
116        if (frame.lineNumber) {
117          str += ":" + frame.lineNumber;
118        }
119
120        if (frame.name) {
121          str = frame.name + "()@" + str;
122        }
123
124        if (str) {
125          output.push(str);
126        }
127        frame = frame.caller;
128      }
129      return `Stack trace: ${output.join("\n")}`;
130    }
131    // Standard JS exception
132    if (e.stack) {
133      let stack = e.stack;
134      return (
135        "JS Stack trace: " +
136        stack.trim().replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1")
137      );
138    }
139
140    return "No traceback available";
141  },
142};
143
144/*
145 * LogMessage
146 * Encapsulates a single log event's data
147 */
148class LogMessage {
149  constructor(loggerName, level, message, params) {
150    this.loggerName = loggerName;
151    this.level = level;
152    /*
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.
157     */
158    if (
159      !params &&
160      message &&
161      typeof message == "object" &&
162      typeof message.valueOf() != "string"
163    ) {
164      this.message = null;
165      this.params = message;
166    } else {
167      // If the message text is empty, or a string, or a String object, normal handling
168      this.message = message;
169      this.params = params;
170    }
171
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();
176  }
177
178  get levelDesc() {
179    if (this.level in Log.Level.Desc) {
180      return Log.Level.Desc[this.level];
181    }
182    return "UNKNOWN";
183  }
184
185  toString() {
186    let msg = `${this.time} ${this.level} ${this.message}`;
187    if (this.params) {
188      msg += ` ${JSON.stringify(this.params)}`;
189    }
190    return `LogMessage [${msg}]`;
191  }
192}
193
194/*
195 * Logger
196 * Hierarchical version.  Logs to all appenders, assigned or inherited
197 */
198
199class Logger {
200  constructor(name, repository) {
201    if (!repository) {
202      repository = Log.repository;
203    }
204    this._name = name;
205    this.children = [];
206    this.ownAppenders = [];
207    this.appenders = [];
208    this._repository = repository;
209
210    this._levelPrefName = null;
211    this._levelPrefValue = null;
212    this._level = null;
213    this._parent = null;
214  }
215
216  get name() {
217    return this._name;
218  }
219
220  get level() {
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;
225      if (lpv) {
226        const levelValue = Log.Level[lpv];
227        if (levelValue) {
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;
231          return levelValue;
232        }
233      } else {
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.
236        this._level = null;
237      }
238    }
239    if (this._level != null) {
240      return this._level;
241    }
242    if (this.parent) {
243      return this.parent.level;
244    }
245    dumpError("Log warning: root logger configuration error: no level defined");
246    return Log.Level.All;
247  }
248  set level(level) {
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.
252      dumpError(
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 ` +
256          `level setter`
257      );
258      return;
259    }
260    this._level = level;
261  }
262
263  get parent() {
264    return this._parent;
265  }
266  set parent(parent) {
267    if (this._parent == parent) {
268      return;
269    }
270    // Remove ourselves from parent's children
271    if (this._parent) {
272      let index = this._parent.children.indexOf(this);
273      if (index != -1) {
274        this._parent.children.splice(index, 1);
275      }
276    }
277    this._parent = parent;
278    parent.children.push(this);
279    this.updateAppenders();
280  }
281
282  manageLevelFromPref(prefName) {
283    if (prefName == this._levelPrefName) {
284      // We've already configured this log with an observer for that pref.
285      return;
286    }
287    if (this._levelPrefName) {
288      dumpError(
289        `The log '${this.name}' is already configured with the ` +
290          `preference '${this._levelPrefName}' - ignoring request to ` +
291          `also use the preference '${prefName}'`
292      );
293      return;
294    }
295    this._levelPrefName = prefName;
296    XPCOMUtils.defineLazyPreferenceGetter(this, "_levelPrefValue", prefName);
297  }
298
299  updateAppenders() {
300    if (this._parent) {
301      let notOwnAppenders = this._parent.appenders.filter(function(appender) {
302        return !this.ownAppenders.includes(appender);
303      }, this);
304      this.appenders = notOwnAppenders.concat(this.ownAppenders);
305    } else {
306      this.appenders = this.ownAppenders.slice();
307    }
308
309    // Update children's appenders.
310    for (let i = 0; i < this.children.length; i++) {
311      this.children[i].updateAppenders();
312    }
313  }
314
315  addAppender(appender) {
316    if (this.ownAppenders.includes(appender)) {
317      return;
318    }
319    this.ownAppenders.push(appender);
320    this.updateAppenders();
321  }
322
323  removeAppender(appender) {
324    let index = this.ownAppenders.indexOf(appender);
325    if (index == -1) {
326      return;
327    }
328    this.ownAppenders.splice(index, 1);
329    this.updateAppenders();
330  }
331
332  _unpackTemplateLiteral(string, params) {
333    if (!Array.isArray(params)) {
334      // Regular log() call.
335      return [string, params];
336    }
337
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]];
342    }
343
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.
348
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];
353    }
354
355    let concat = string[0];
356    for (let i = 0; i < params.length; i++) {
357      concat += `\${${i}}${string[i + 1]}`;
358    }
359    return [concat, params];
360  }
361
362  log(level, string, params) {
363    if (this.level > level) {
364      return;
365    }
366
367    // Hold off on creating the message object until we actually have
368    // an appender that's responsible.
369    let message;
370    let appenders = this.appenders;
371    for (let appender of appenders) {
372      if (appender.level > level) {
373        continue;
374      }
375      if (!message) {
376        [string, params] = this._unpackTemplateLiteral(string, params);
377        message = new LogMessage(this._name, level, string, params);
378      }
379      appender.append(message);
380    }
381  }
382
383  fatal(string, ...params) {
384    this.log(Log.Level.Fatal, string, params);
385  }
386  error(string, ...params) {
387    this.log(Log.Level.Error, string, params);
388  }
389  warn(string, ...params) {
390    this.log(Log.Level.Warn, string, params);
391  }
392  info(string, ...params) {
393    this.log(Log.Level.Info, string, params);
394  }
395  config(string, ...params) {
396    this.log(Log.Level.Config, string, params);
397  }
398  debug(string, ...params) {
399    this.log(Log.Level.Debug, string, params);
400  }
401  trace(string, ...params) {
402    this.log(Log.Level.Trace, string, params);
403  }
404}
405
406/*
407 * LoggerRepository
408 * Implements a hierarchy of Loggers
409 */
410
411class LoggerRepository {
412  constructor() {
413    this._loggers = {};
414    this._rootLogger = null;
415  }
416
417  get rootLogger() {
418    if (!this._rootLogger) {
419      this._rootLogger = new Logger("root", this);
420      this._rootLogger.level = Log.Level.All;
421    }
422    return this._rootLogger;
423  }
424  set rootLogger(logger) {
425    throw new Error("Cannot change the root logger");
426  }
427
428  _updateParents(name) {
429    let pieces = name.split(".");
430    let cur, parent;
431
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++) {
436      if (cur) {
437        cur += "." + pieces[i];
438      } else {
439        cur = pieces[i];
440      }
441      if (cur in this._loggers) {
442        parent = cur;
443      }
444    }
445
446    // if we didn't assign a parent above, there is no parent
447    if (!parent) {
448      this._loggers[name].parent = this.rootLogger;
449    } else {
450      this._loggers[name].parent = this._loggers[parent];
451    }
452
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);
457      }
458    }
459  }
460
461  /**
462   * Obtain a named Logger.
463   *
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.
467   *
468   * @return Logger
469   */
470  getLogger(name) {
471    if (name in this._loggers) {
472      return this._loggers[name];
473    }
474    this._loggers[name] = new Logger(name, this);
475    this._updateParents(name);
476    return this._loggers[name];
477  }
478
479  /**
480   * Obtain a Logger that logs all string messages with a prefix.
481   *
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.
486   *
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.
490   *
491   * @param name
492   *        (string) The Logger to retrieve.
493   * @param prefix
494   *        (string) The string to prefix each logged message with.
495   */
496  getLoggerWithMessagePrefix(name, prefix) {
497    let log = this.getLogger(name);
498
499    let proxy = Object.create(log);
500    proxy.log = (level, string, params) => {
501      if (Array.isArray(string) && Array.isArray(params)) {
502        // Template literal.
503        // We cannot change the original array, so create a new one.
504        string = [prefix + string[0]].concat(string.slice(1));
505      } else {
506        string = prefix + string; // Regular string.
507      }
508      return log.log(level, string, params);
509    };
510    return proxy;
511  }
512}
513
514/*
515 * Formatters
516 * These massage a LogMessage into whatever output is desired.
517 */
518
519// Basic formatter that doesn't do anything fancy.
520class BasicFormatter {
521  constructor(dateFormat) {
522    if (dateFormat) {
523      this.dateFormat = dateFormat;
524    }
525    this.parameterFormatter = new ParameterFormatter();
526  }
527
528  /**
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
534   * to the message.
535   */
536  formatText(message) {
537    let params = message.params;
538    if (typeof params == "undefined") {
539      return message.message || "";
540    }
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";
544
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
549      let subDone = false;
550      let regex = /\$\{(\S*?)\}/g;
551      let textParts = [];
552      if (message.message) {
553        textParts.push(
554          message.message.replace(regex, (_, sub) => {
555            // ${foo} means use the params['foo']
556            if (sub) {
557              if (pIsObject && sub in message.params) {
558                subDone = true;
559                return this.parameterFormatter.format(message.params[sub]);
560              }
561              return "${" + sub + "}";
562            }
563            // ${} means use the entire params object.
564            subDone = true;
565            return this.parameterFormatter.format(message.params);
566          })
567        );
568      }
569      if (!subDone) {
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);
574        }
575      }
576      return textParts.join(": ");
577    }
578    return undefined;
579  }
580
581  format(message) {
582    return (
583      message.time +
584      "\t" +
585      message.loggerName +
586      "\t" +
587      message.levelDesc +
588      "\t" +
589      this.formatText(message)
590    );
591  }
592}
593
594/**
595 * Test an object to see if it is a Mozilla JS Error.
596 */
597function isError(aObj) {
598  return (
599    aObj &&
600    typeof aObj == "object" &&
601    "name" in aObj &&
602    "message" in aObj &&
603    "fileName" in aObj &&
604    "lineNumber" in aObj &&
605    "stack" in aObj
606  );
607}
608
609/*
610 * Parameter Formatters
611 * These massage an object used as a parameter for a LogMessage into
612 * a string representation of the object.
613 */
614
615class ParameterFormatter {
616  constructor() {
617    this._name = "ParameterFormatter";
618  }
619
620  format(ob) {
621    try {
622      if (ob === undefined) {
623        return "undefined";
624      }
625      if (ob === null) {
626        return "null";
627      }
628      // Pass through primitive types and objects that unbox to primitive types.
629      if (
630        (typeof ob != "object" || typeof ob.valueOf() != "object") &&
631        typeof ob != "function"
632      ) {
633        return ob;
634      }
635      if (ob instanceof Ci.nsIException) {
636        return `${ob} ${Log.stackTrace(ob)}`;
637      } else if (isError(ob)) {
638        return Log._formatError(ob);
639      }
640      // Just JSONify it. Filter out our internal fields and those the caller has
641      // already handled.
642      return JSON.stringify(ob, (key, val) => {
643        if (INTERNAL_FIELDS.has(key)) {
644          return undefined;
645        }
646        return val;
647      });
648    } catch (e) {
649      dumpError(
650        `Exception trying to format object for log message: ${Log.exceptionStr(
651          e
652        )}`
653      );
654    }
655    // Fancy formatting failed. Just toSource() it - but even this may fail!
656    try {
657      return ob.toSource();
658    } catch (_) {}
659    try {
660      return String(ob);
661    } catch (_) {
662      return "[object]";
663    }
664  }
665}
666
667/*
668 * Appenders
669 * These can be attached to Loggers to log to different places
670 * Simply subclass and override doAppend to implement a new one
671 */
672
673class Appender {
674  constructor(formatter) {
675    this.level = Log.Level.All;
676    this._name = "Appender";
677    this._formatter = formatter || new BasicFormatter();
678  }
679
680  append(message) {
681    if (message) {
682      this.doAppend(this._formatter.format(message));
683    }
684  }
685
686  toString() {
687    return `${this._name} [level=${this.level}, formatter=${this._formatter}]`;
688  }
689}
690
691/*
692 * DumpAppender
693 * Logs to standard out
694 */
695
696class DumpAppender extends Appender {
697  constructor(formatter) {
698    super(formatter);
699    this._name = "DumpAppender";
700  }
701
702  doAppend(formatted) {
703    dump(formatted + "\n");
704  }
705}
706
707/*
708 * ConsoleAppender
709 * Logs to the javascript console
710 */
711
712class ConsoleAppender extends Appender {
713  constructor(formatter) {
714    super(formatter);
715    this._name = "ConsoleAppender";
716  }
717
718  // XXX this should be replaced with calls to the Browser Console
719  append(message) {
720    if (message) {
721      let m = this._formatter.format(message);
722      if (message.level > Log.Level.Warn) {
723        Cu.reportError(m);
724        return;
725      }
726      this.doAppend(m);
727    }
728  }
729
730  doAppend(formatted) {
731    Services.console.logStringMessage(formatted);
732  }
733}
734
735Object.assign(Log, {
736  LogMessage,
737  Logger,
738  LoggerRepository,
739
740  BasicFormatter,
741
742  Appender,
743  DumpAppender,
744  ConsoleAppender,
745
746  ParameterFormatter,
747});
748