'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$/);
if(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) {
if(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 || {};
if(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);
});
if(!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') {
if(this.formatters.indexOf(name) === -1) {
this.formatters.push(name);
}
} else if(type === 'handler') {
if(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;