Home Manual Reference Source Test

src/Logger.js

import Emitter from 'events';
import { EOL } from 'os';
import chalk from 'chalk';
import pad from 'left-pad';

/**
 * The logger class. **Please note that, when importing default from this module, you will always
 * get a shared Logger instance.**
 * @see {@link logger}
 */
export default class Logger extends Emitter {

  /**
   * The default log types.
   * @type {string[]} Default log types.
   */
  static get defaultTypes() {
    return ['error', 'warn', 'info', 'debug'];
  }

  /**
   * Creates a new Logger with the given options.
   * @param {Object} [options] The options to use.
   * @param {string[]} [options.types=Logger.defaultTypes] The log types the new
   * instance should have.
   * @param {string[]|string} [options.scope=[]] The logger's scope.
   * @param {boolean} [options.timestamp=true] If timestamps should be printed.
   */
  constructor({ types = Logger.defaultTypes, scope = [], timestamp = true } = {}) {
    super();

    /**
     * The log types available.
     * @type {string[]}
     */
    this._types = types;

    /**
     * If messages should be prefixed by a timestamp.
     * @type {boolean}
     */
    this._printTimestamp = timestamp;

    /**
     * The log level where no messages are printed.
     * @type {number}
     */
    this.LEVEL_SILENT = 0;

    this._types
      .forEach((type, i) => {
        /**
         * Prints the given messages for the given message types.
         * @param {...*} messages The messages to print.
         * @return {Logger} The logger instance.
         * @emits {string} Emits an event named after `type` with the message printed.
         *
         * @example <caption>Subscribing to specific log types</caption>
         * Logger.on('warn', msg => console.log('Warning:', msg))
         *   .warn('Test warning');
         * // Will print "Waring: Test warning" to the console.
         */
        this[type] = (...messages) => this.emit(type, this._format(messages));

        const typeName = type.toUpperCase();

        /**
         * The log levels available.
         * @type {number}
         */
        this['LEVEL_' + typeName] = i + 1; // eslint-disable-line prefer-template
      });

    /**
     * The current logger scope.
     * @type {string[]}
     */
    this._scope = Array.isArray(scope) ? scope : ((scope && [scope]) || []);

    /**
     * The colors used.
     * @type {chalk}
     */
    this.colors = chalk;
  }

  /**
   * The current logger scope.
   * @type {string[]}
   */
  get scope() {
    return this._scope;
  }

  /**
   * Creates a child logger instance. Basically creates a new logger instance with the same setup
   * with `name` as an additional scope entry.
   * @param {string} name The child logger's name.
   * @return {Logger} A new logger instance.
   */
  createChild(name) {
    const child = new Logger({
      types: this._types,
      scope: this.scope.concat(name),
      timestamp: this._printTimestamp,
    });

    this._types.forEach(type => {
      child.on(type, e => this.emit(type, e));
    });

    return child;
  }

  /**
   * A formatted timestamp.
   */
  get timestamp() {
    const now = new Date();

    return `[${this.colors.gray([
      now.getHours(),
      now.getMinutes(),
      now.getSeconds(),
    ].map(n => pad(n, 2, '0')).join(':'))}]`;
  }

  /**
   * The message prefix. Override this getter in your own logger class to customize output.
   * @type {string}
   */
  get prefix() {
    return (this._printTimestamp ? [this.timestamp] : []).concat(this._scope);
  }

  /**
   * Creates a log message from some messages, prefixed and postfixed with a newline.
   * @param {*[]} messages The messages to format.
   * @return {string} The resulting message.
   */
  _format(messages) {
    return `${
      this.prefix.concat(messages).join(' ')
    }${EOL}`;
  }

  /**
   * Returns the log types that should be handled for the specific log level.
   * @param {number} logLevel The log level to use.
   * @return {string[]} The log types to handle.
   */
  typesForLevel(logLevel) {
    return this._types
      .filter((_, i) => i <= (logLevel - 1));
  }

  /**
   * Writes all logs to a writable stream.
   * @param {stream~Writable} stream The stream to write to.
   * @param {Object} [options] The options to use.
   * @param {number} [options.logLevel=3] The log level from which to write.
   * @return {Logger} The logger instance.
   *
   * @example <caption>Pipe logger output to stdout</caption>
   * Logger.pipe(process.stdout);
   *
   * @example <caption>Pipe with specific log level</caption>
   * Logger.pipe(process.stdout, { logLevel: Logger.LEVEL_ERROR });
   * // In this case, only error messages will be printed to stdout
   */
  pipe(stream, options = {}) {
    const logLevel = options.logLevel === undefined ? 3 : options.logLevel;
    const listener = m => stream.write(m);

    this.typesForLevel(logLevel)
      .forEach(level => this.on(level, listener));

    stream.once('finish', () => this.typesForLevel(logLevel)
      .forEach(level => this.removeListener(level, listener))
    );

    return this;
  }

}