src/services/$LogProvider.js
/**
* @module $LogProvider.js
* @author Joe Groseclose <@benderTheCrime>
* @date 8/23/2015
*/
// System Modules
import fs from 'fs';
import chalk, { bold } from 'chalk';
const CWD = process.cwd(),
LOG_LEVELS = {
error: 'ERROR',
warn: 'WARN',
debug: 'DEBUG',
info: 'INFO'
},
DEFAULT_LOG_FILE = 'angie.log';
/**
* @desc $LogProvider is the only class in the Angie Logging module. This module
* allows for logging to the terminal (with a little flavor).
*
* This can also be referenced as Log
* @since 0.0.2
* @access public
* @example new $LogProvider();
*/
class $LogProvider {
/**
* @desc Instantiate a logger for use in the current module.
* @since 0.0.2
* @param {string|object} outfile The file to which messages are logged, or
* a hash of options to pass the logger
* @param {string} outfile.outfile [param=process.cwd() + '/angie.log'] The file
* to which messages are logged
* @param {string} outfile.file [param=process.cwd() + '/angie.log'] The file to
* which messages are logged. All files will be relative to process.cwd().
* @param {string} outfile.name The name of the logger to be recorded in the
* log file
* @param {boolean} outfile.timestamp [param=true] Whether or not to include
* a timestamp in the log output
* @param {string} outfile.level [param='DEBUG'] The recorded log message
* level. Possible options: debug, error, info, warn
* @param {string} outfile.logLevel [param='DEBUG'] The recorded log message
* level. Possible options: debug, error, info, warn
* @param {boolean} outfile.silent [param=false] The recorded log message
* level. Possible options: debug, error, info, warn
* @param {string} name The name of the logger to be recorded in the
* log file
* @param {boolean} timestamp [param=true] Whether or not to include a
* timestamp in the log output
* @param {string} level [param='DEBUG'] The recorded log message level.
* Possible options: debug, error, info, warn
* @param {boolean} silent [param=false] Should the log output also log to
* the terminal?
* @access public
* @example new $LogProvider(output.log, 'test', true, 'DEBUG', false);
*/
constructor(
outfile = DEFAULT_LOG_FILE,
name = null,
timestamp = true,
level = [ 'DEBUG' ],
silent = false,
messages = []
) {
// Account for object arguments
[
this.$$outfile,
this.$$name,
this.$$timestamp,
this.$$silent,
this.$$messages
] = typeof outfile === 'object' ? [
`${CWD}/${
outfile.hasOwnProperty('outfile') || outfile.hasOwnProperty('file') ?
removeLeadingSlashes(outfile.outfile || outfile.file) :
DEFAULT_LOG_FILE
}`,
outfile.hasOwnProperty('name') ? outfile.name : name,
outfile.hasOwnProperty('timestamp') ? outfile.timestamp : timestamp,
outfile.hasOwnProperty('silent') ? outfile.silent : silent,
outfile.hasOwnProperty('messages') ? outfile.messages : messages
] : [
`${CWD}/${removeLeadingSlashes(outfile)}`,
name,
timestamp,
silent,
messages
];
// Check the log level and make sure it is an acceptable value
if (typeof outfile === 'object') {
if (outfile.hasOwnProperty('level')) {
level = outfile.level;
} else if (outfile.hasOwnProperty('logLevel')) {
level = outfile.logLevel;
} else if (outfile.hasOwnProperty('levels')) {
level = outfile.levels;
} else if (outfile.hasOwnProperty('logLevels')) {
level = outfile.logLevels;
}
}
// Cast level to an Array if it is a string
level = typeof level === 'string' ? [ level ] : level;
// Make sure we properly set up the level
this.$setLevels(level);
// Backup the original level type
this.$$initialLevel = [ ...this.$$level ];
// Observe the messages array, logging a record each time a message is
// added
let me = this;
Object.observe(this.$$messages, function() {
let message = me.$$messages.shift();
// Forcibly add a hard return if one does not exist
message = !/.*(\r|\n)$/.test(message) ? `${message}\n` : message;
// Write to the output file
fs.appendFile(me.$$outfile, message);
}, [ 'add' ]);
}
/**
* @desc Logs an error to an outfile and to the terminal (unless otherwise
* specified). Allows any any arguments.
* @since 0.9.7
* @access public
* @example new $LogProvider({ ...args }).error('test');
*/
error() {
return this.$$filter('error', ...arguments);
}
/**
* @desc Logs a warning to an outfile and to the terminal (unless otherwise
* specified). Allows any any arguments.
* @since 0.9.7
* @access public
* @example new $LogProvider({ ...args }).warn('test');
*/
warn() {
return this.$$filter('warn', ...arguments);
}
/**
* @desc Logs a debug message to an outfile and to the terminal (unless
* otherwise specified). Allows any any arguments.
* @since 0.9.7
* @access public
* @example new $LogProvider({ ...args }).debug('test');
*/
debug() {
return this.$$filter('debug', ...arguments);
}
/**
* @desc Logs info to an outfile and to the terminal (unless otherwise
* specified)
* @since 0.9.7
* @access public
* @example new $LogProvider({ ...args }).info('test');
*/
info() {
return this.$$filter('info', ...arguments);
}
/**
* @desc Set the file to which the logger records. All files will be
* relative to `process.cwd()`.
* @since 0.0.2
* @param {string} o [param=process.cwd() + '/angie.log'] The file to which
* messages are logged. All files will be relative to `process.cwd()`.
* @access private
* @example new $LogProvider().$setlogger('./angie.log');
*/
$setOutfile(o = DEFAULT_LOG_FILE) {
this.$$outfile = `${CWD}/${removeLeadingSlashes(o)}`;
return this;
}
/**
* @desc Set the name of the logger recorded
* @since 0.9.7
* @param {string} n The name of the logger to be recorded in the log file
* @access private
* @example new $LogProvider().$setName('logger');
*/
$setName(n) {
// Check this explicitly to make sure it is not called with an empty
if (n) {
this.$$name = n;
}
return this;
}
/**
* @desc Set the logger timestamp preference
* @since 0.0.2
* @param {boolean} t [param=true] Whether or not to include a timestamp in
* the log output
* @access private
* @example new $LogProvider().$setTimestamp(true);
*/
$setTimestamp(t = true) {
this.$$timestamp = t;
return this;
}
/**
* @desc Set the log level
* @since 0.0.2
* @param {string} l The recorded log message level.
* Possible options: debug, error, info, warn
* @access private
* @example new $LogProvider().$setLevel('debug');
*/
$setLevel(l) {
// Just a little insurance...
if (!(this.$$level instanceof Set)) {
this.$$level = new Set();
}
// If l exists, check to see if it is a valid log level and set the level
if (l) {
let level = LOG_LEVELS[
LOG_LEVELS.hasOwnProperty(l) ? l : 'debug'
];
this.$$level.add(level);
}
return this;
}
/**
* @desc Set the log level to many level strings
* @since 0.9.8
* @param {Array} l The recorded log message level array.
* Possible options: debug, error, info, warn
* @access private
* @example new $LogProvider().$setLevels([ 'debug', 'info' ]);
*/
$setLevels(l) {
let me = this;
// This function only takes an Array of level argument strings
if (l instanceof Array && l.length) {
this.$$level = new Set();
// Attempt to add the level to the set for each level
l.forEach(function(level) {
me.$setLevel(level);
});
}
return this;
}
/**
* @desc Set the logger silent preference
* @since 0.0.2
* @param {boolean} s [param=false] Should the log output also log to
* the terminal?
* @access private
* @example new $LogProvider().$setTimestamp(false);
*/
$setSilent(s = false) {
this.$$silent = s;
return this;
}
/**
* @desc Checks to see that the specified log level in the instantiated
* logger is observed
* @since 0.9.7
* @access private
*/
$$filter() {
let level = LOG_LEVELS[ arguments[0] ];
// Check to see that the log level of the declared logger matches that
// of the called method
if (level && this.$$level.has(level)) {
return this.$$logger.apply(this, arguments);
}
return this;
}
/**
* @desc $LogProvider.prototpye$$logger will add a log statement for each
* call that is made. It pushes messages to an asynchronous queue, which
* will execute as messages are added. Note: if this method is called
* explicitly, the first argument will have to be a valid log level
* @since 0.0.2
* @param {string} level The level of log output to put into the log
* @access private
* @example new $LogProvider(output.log, true, 'DEBUG', false).logger('test');
*/
$$logger() {
let args = Array.prototype.slice.call(arguments),
level = args.shift();
if (!LOG_LEVELS.hasOwnProperty(level)) {
$LogProvider.warn(
`${this.$$name ? `[${chalk.cyan(this.$$name)}] ` : ''}$$logger ` +
'called explicitly without a valid log level'
);
return this;
}
// Modify the output string
let message = new Array(args);
message.unshift(
`${this.$$timestamp ? `[${new Date().toString()}] ` : ''}` +
`[${LOG_LEVELS[ level ]}]` +
`${this.$$name ? ` [${this.$$name}]` : ''} :`
);
// Avoid space before return
message = `${message.join(' ')}\r`;
// Push a message to the Array of messages obseved
this.$$messages.push(message);
// Log the message in the terminal as well unless silent
if (this.$$silent !== true) {
$LogProvider[ level ](args);
}
return this;
}
/**
* @desc Reset the log level to the original specified log level
* @since 0.9.7
* @access private
*/
$$resetLevels() {
return this.$setLevels(this.$$initialLevel);
}
/**
* @desc A wrapper for ERROR output
* @since 0.0.2
* @param {string} s One or many string inputs
* @access public
* @example new $LogProvider.error('test');
*/
static error() {
let args = carriage(...arguments);
if (args && args[0].stack) {
args[0] = args[0].stack;
}
args.unshift(
chalk.red(`[${new Date().toString()}] [ERROR] :`));
args.push('\r');
console.error(bold.apply(null, args));
}
/**
* @desc A wrapper for WARN output
* @since 0.0.2
* @param {string} s One or many string inputs
* @access public
* @example new $LogProvider.warn('test');
*/
static warn() {
let args = carriage(...arguments);
args.unshift(chalk.yellow(`[${new Date().toString()}] [WARN] :`));
args.push('\r');
console.warn(bold.apply(null, args));
}
/**
* @desc A wrapper for DEBUG output
* @since 0.0.2
* @param {string} s One or many string inputs
* @access public
* @example new $LogProvider.debug('test');
*/
static debug() {
let args = carriage(...arguments);
args.unshift(`[${new Date().toString()}] [DEBUG] :`);
args.push('\r');
console.log(bold.apply(null, args));
}
/**
* @desc A wrapper for INFO output
* @since 0.0.2
* @param {string} s One or many string inputs
* @access public
* @example new $LogProvider.info('test');
*/
static info() {
let args = carriage(...arguments);
args.unshift(chalk.green(`[${new Date().toString()}] [INFO] :`));
args.push('\r');
console.log(bold.apply(null, args));
}
}
/**
* @desc Helper function to drop hard returns in between arguments
* @since 0.0.2
* @access private
*/
function carriage() {
let args = Array.prototype.slice.call(arguments);
return args.map((v) => v && v.replace ? v.replace(/(\r|\n)/g, ' ') : v);
}
/**
* @desc Helper function to remove leading argument slashes
* @since 0.9.7
* @access private
*/
function removeLeadingSlashes(str) {
return str.replace(/(^(\/))/, '');
}
// Declare the module in the Angie app space
if (
typeof app === 'object' &&
typeof app.factory === 'function'
) {
app.factory('$Log', $LogProvider);
}
export default $LogProvider;