utils_create-logger.js

/**
 * @file create-logger.js
 * @module create-logger
 */
import window from 'global/window';

// This is the private tracking variable for the logging history.
let history = [];

/**
 * Log messages to the console and history based on the type of message
 *
 * @private
 * @param  {string} name
 *         The name of the console method to use.
 *
 * @param  {Object} log
 *         The arguments to be passed to the matching console method.
 *
 * @param {string} [styles]
 *        styles for name
 */
const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
  const lvl = log.levels[level];
  const lvlRegExp = new RegExp(`^(${lvl})$`);

  let resultName = name;

  if (type !== 'log') {

    // Add the type to the front of the message when it's not "log".
    args.unshift(type.toUpperCase() + ':');
  }

  if (styles) {
    resultName = `%c${name}`;
    args.unshift(styles);
  }

  // Add console prefix after adding to history.
  args.unshift(resultName + ':');

  // Add a clone of the args at this point to history.
  if (history) {
    history.push([].concat(args));

    // only store 1000 history entries
    const splice = history.length - 1000;

    history.splice(0, splice > 0 ? splice : 0);
  }

  // If there's no console then don't try to output messages, but they will
  // still be stored in history.
  if (!window.console) {
    return;
  }

  // Was setting these once outside of this function, but containing them
  // in the function makes it easier to test cases where console doesn't exist
  // when the module is executed.
  let fn = window.console[type];

  if (!fn && type === 'debug') {
    // Certain browsers don't have support for console.debug. For those, we
    // should default to the closest comparable log.
    fn = window.console.info || window.console.log;
  }

  // Bail out if there's no console or if this type is not allowed by the
  // current logging level.
  if (!fn || !lvl || !lvlRegExp.test(type)) {
    return;
  }

  fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
};

export default function createLogger(name, delimiter = ':', styles = '') {
  // This is the private tracking variable for logging level.
  let level = 'info';

  // the curried logByType bound to the specific log and history
  let logByType;

  /**
   * Logs plain debug messages. Similar to `console.log`.
   *
   * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
   * of our JSDoc template, we cannot properly document this as both a function
   * and a namespace, so its function signature is documented here.
   *
   * #### Arguments
   * ##### *args
   * *[]
   *
   * Any combination of values that could be passed to `console.log()`.
   *
   * #### Return Value
   *
   * `undefined`
   *
   * @namespace
   * @param    {...*} args
   *           One or more messages or objects that should be logged.
   */
  const log = function(...args) {
    logByType('log', level, args);
  };

  // This is the logByType helper that the logging methods below use
  logByType = LogByTypeFactory(name, log, styles);

  /**
   * Create a new subLogger which chains the old name to the new name.
   *
   * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
   * ```js
   *  mylogger('foo');
   *  // > VIDEOJS: player: foo
   * ```
   *
   * @param {string} subName
   *        The name to add call the new logger
   * @param {string} [subDelimiter]
   *        Optional delimiter
   * @param {string} [subStyles]
   *        Optional styles
   * @return {Object}
   */
  log.createLogger = (subName, subDelimiter, subStyles) => {
    const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
    const resultStyles = subStyles !== undefined ? subStyles : styles;
    const resultName = `${name} ${resultDelimiter} ${subName}`;

    return createLogger(resultName, resultDelimiter, resultStyles);
  };

  /**
   * Create a new logger.
   *
   * @param {string} newName
   *        The name for the new logger
   * @param {string} [newDelimiter]
   *        Optional delimiter
   * @param {string} [newStyles]
   *        Optional styles
   * @return {Object}
   */
  log.createNewLogger = (newName, newDelimiter, newStyles) => {
    return createLogger(newName, newDelimiter, newStyles);
  };

  /**
   * Enumeration of available logging levels, where the keys are the level names
   * and the values are `|`-separated strings containing logging methods allowed
   * in that logging level. These strings are used to create a regular expression
   * matching the function name being called.
   *
   * Levels provided by Video.js are:
   *
   * - `off`: Matches no calls. Any value that can be cast to `false` will have
   *   this effect. The most restrictive.
   * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
   *   `log.warn`, and `log.error`).
   * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
   * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
   * - `warn`: Matches `log.warn` and `log.error` calls.
   * - `error`: Matches only `log.error` calls.
   *
   * @type {Object}
   */
  log.levels = {
    all: 'debug|log|warn|error',
    off: '',
    debug: 'debug|log|warn|error',
    info: 'log|warn|error',
    warn: 'warn|error',
    error: 'error',
    DEFAULT: level
  };

  /**
   * Get or set the current logging level.
   *
   * If a string matching a key from {@link module:log.levels} is provided, acts
   * as a setter.
   *
   * @param  {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
   *         Pass a valid level to set a new logging level.
   *
   * @return {string}
   *         The current logging level.
   */
  log.level = (lvl) => {
    if (typeof lvl === 'string') {
      if (!log.levels.hasOwnProperty(lvl)) {
        throw new Error(`"${lvl}" in not a valid log level`);
      }
      level = lvl;
    }
    return level;
  };

  /**
   * Returns an array containing everything that has been logged to the history.
   *
   * This array is a shallow clone of the internal history record. However, its
   * contents are _not_ cloned; so, mutating objects inside this array will
   * mutate them in history.
   *
   * @return {Array}
   */
  log.history = () => history ? [].concat(history) : [];

  /**
   * Allows you to filter the history by the given logger name
   *
   * @param {string} fname
   *        The name to filter by
   *
   * @return {Array}
   *         The filtered list to return
   */
  log.history.filter = (fname) => {
    return (history || []).filter((historyItem) => {
      // if the first item in each historyItem includes `fname`, then it's a match
      return new RegExp(`.*${fname}.*`).test(historyItem[0]);
    });
  };

  /**
   * Clears the internal history tracking, but does not prevent further history
   * tracking.
   */
  log.history.clear = () => {
    if (history) {
      history.length = 0;
    }
  };

  /**
   * Disable history tracking if it is currently enabled.
   */
  log.history.disable = () => {
    if (history !== null) {
      history.length = 0;
      history = null;
    }
  };

  /**
   * Enable history tracking if it is currently disabled.
   */
  log.history.enable = () => {
    if (history === null) {
      history = [];
    }
  };

  /**
   * Logs error messages. Similar to `console.error`.
   *
   * @param {...*} args
   *        One or more messages or objects that should be logged as an error
   */
  log.error = (...args) => logByType('error', level, args);

  /**
   * Logs warning messages. Similar to `console.warn`.
   *
   * @param {...*} args
   *        One or more messages or objects that should be logged as a warning.
   */
  log.warn = (...args) => logByType('warn', level, args);

  /**
   * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
   * log if `console.debug` is not available
   *
   * @param {...*} args
   *        One or more messages or objects that should be logged as debug.
   */
  log.debug = (...args) => logByType('debug', level, args);

  return log;
}