all files / lib/ context.js

91.23% Statements 52/57
69.44% Branches 25/36
84.62% Functions 11/13
91.23% Lines 52/57
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236                                                                                                                                                                  20× 20× 20× 20× 16×                                                                                                                                                                                                              
'use strict';
 
const _ = require('lodash');
const EE = require('eventemitter3');
const pp = require('plugin-party');
const path = require('path');
const fs = require('fs');
 
/**
 * Plugin is used to offer more possiblities with catlog.
 *
 * @class Plugin
 */
 
 
 /**
  * A method to apply options for plugin
  *
  * @memberof Plugin#
  * @param {Object} options - passed options
  * @returns {void}
  */
 
/**
 * Plugin type name
 *
 * @type {string}
 * @memberof Plugin#
 */
 
/**
 * Plugin name
 *
 * @type {string}
 * @memberof Plugin#
 */
 
/**
 * @typedef {Object} ContextOptions
 * @property {PluginOptions[]} - options for plugins. Except `name` and `type`, other properties may be required for many plugins.
 */
 
/**
 * @typedef {Object} PluginOptions
 * @property {!string} type - plugin type name, that is `formatter` or `handler`
 * @property {!string} name - unique plugin name
 *
 */
 
/**
 * @typedef {Object} LoggerOptions
 * @property {!string} name - method name on logger
 * @property {number} [level=100] - a positive integer that represents its priority.
 * @property {string} topic - a human-readable label. Copied from `name` if not given
 * @property {boolean} [stack=true] - if this method should capture the calling stack
 * @property {boolean} [trace=false] - if this method should output stack trace
 */
 
//fallback formatter
const fallback = {
  fmt: function () {
    return false;
  },
  configure: function () {}
};
 
/**
 * Working context, which stores all plugins and configrations, can generate multiple loggers.
 * A context has two types of plugins:
 *
 * 1. Handler: a handler recieve log info and process. For example, a {@link FSHandler} can persists log data onto file system with rolling datetime as fileanme. A log method passes log data to handlers through event emitter asynchronously, so that a compliated handler won't affect performance.
 * 2. Formatter: a formatter is used to render log text with raw log data. Most simple formatter is {@link NativeFormatter}, which only output with `util.format` shipped by Node.js.
 *
 * By default, a context is equipped with a {@link SimpleFormatter} and {@link FSHandler}.
 *
 * @class
 */
class Context extends EE {
 
  /**
   * Default constructor
   *
   * @param {ContextOptions} [options] - contxt options
   * @constructor
   */
  constructor(options) {
    super();
    this.options = {};
    pp(this);
 
    //load default plugins
    _.forEach(fs.readdirSync(path.join(__dirname, 'plugins')), function (name) {
      const m = name.match(/^(.+)\.js$/);
      Eif(m && m.length === 2) {
        const Plugin = require('./plugins/' + m[1]);
        if(pp.isPlugin(Plugin)) {
          this.register(Plugin);
        }
      }
    }, this);
 
    this.formatters = [];
    this.handlers = [];
 
 
    this.configure(options || {
      plugins: [
        {type: 'formatter', name: 'simple'},
        {type: 'handler', name: 'fs'}
      ]
    });
 
    this.addLogMethod = require('./log_method');
  }
 
  /**
   * Update context with options.
   *
   * @param {ContextOptions} options - contxt options
   * @returns {Context} - current context
   */
  configure(options) {
    this.options = _.extend(this.options, options || {});
    //normalize formatter and handler
    _.each(this.options.plugins, function (p) {
      Iif(pp.isPlugin(p)) {
        this.register(p);
        this.install(p.pluginType, p.pluginName);
      } else {
        this.install(p.type, p.name, p);
      }
    }, this);
    return this;
  }
 
  /**
   * Create a logger with options. A logger can have abitary log methods. By defualt, a logger only has `info`, `debug`, `info`, `warn`, `error` methods.
   * Each method has its own attributes specified by {@link LoggerOptions}
   *
   *
   * @param {LoggerOptions} [options] - options for a logger
   * @returns {Logger} - created logger
   */
  logger(options) {
    options = options || {};
    Iif(typeof options === 'string') {
      options = {
        category: options
      };
    }
    const self = this;
    const target = options.category ? this.addLogMethod('debug', {
      target: 'stdout',
      // level lower than 80 causes some handlers to skip log events from this logger
      level: 70,
      category: options.category,
      formatter: 'filtered',
      topic: 'filtered_debug'
    }) : {};
 
    //merge methods
    const methods = _.defaults(this.constructor.defaults.methods, options.methods || {});
    _.forOwn(methods, function (v, k) {
      self.addLogMethod(k, v, target);
    });
    Eif(!methods.log) {
      //map log to info
      this.addLogMethod('log', _.extend({topic: 'info'}, methods.info), target);
    }
    target.configure = function (fn) {
      return fn ? fn.call(self, self) : false;
    };
    return target;
  }
 
  /**
   * Setup a plugin
   *
   * @param {string} type - plugin type
   * @param {string} name - plugin name
   * @param {PluginOptions} options - plugin options
   * @returns {Context} - context it self
   */
  install(type, name, options) {
    try {
      this.plugin(type, name).configure(options, this);
      if(type === 'formatter') {
        Eif(this.formatters.indexOf(name) === -1) {
          this.formatters.push(name);
        }
      } else Eif(type === 'handler') {
        Eif(this.handlers.indexOf(name) === -1) {
          this.handlers.push(name);
        }
      }
    } catch(e) {
      console.error(e.stack);
      //Do nothing
    }
    return this;
  }
 
  /**
   * Get a registerd formatter by name. If not formatter has been defined before, a fallback formatter will be returned, which output nothing to stdout/stderr.
   *
   * @param {string} name - formatter name
   * @returns {Formatter} - the formatter found
   */
  formatter(name) {
    name = name || this.formatters[0];
    if(name) {
      return this.plugin('formatter', name);
    }
    return fallback;
  }
 
  /**
   * Default method configs. See {@link defaults.js} for detailed config.
   * @type {Object}
   */
  static get defaults() {
    return require('./defaults');
  }
 
  /**
   * Shared instance with default configuration
   * @type {Context}
   *
   */
  static get one() {
    return new Context();
  }
}
 
module.exports = Context;