bugologger
Version:
An easy-to-use and lightweight Node.JS logger with colours & styles, log files, and timestamps.
262 lines (222 loc) • 8.62 kB
JavaScript
/**
* @module leekslazylogger
* @author eartharoid <contact@eartharoid.me>
* @description An easy-to-use and lightweight Node.JS logger with file support, colours, and timestamps.
* @copyright 2020 Isaac Saunders (eartharoid)
* @license MIT
*/
const { Console } = console;
const merge = require('@eartharoid/deep-merge'); // for merging defaults with passed options
const dtf = require('@eartharoid/dtf'); // for the timestamp & file name
const leeks = require('leeks.js'); // like chalk, terminal colours & styles
const pkg = require('../package.json'); // for version and author info etc
const fs = require('fs'); // if you don't know what this is you shouldn't be reading this
const defaults = require('./defaults'); // default options
const codes = require('./codes'); // short codes
const {
join,
resolve
} = require('path');
const {
plural,
replaceCodes,
strip,
fgColourType,
bgColourType,
capitalise,
} = require('./utils');
class LoggerError extends Error {
constructor(message) {
console.log(leeks.colours.yellow(`[LOGGER] Logger error thrown; perhaps you should read the documentation at ${pkg.homepage} ..?`));
super(message);
}
}
/** The Logger */
class Logger {
/**
* @description Create a new Logger
* @param {object} o - customise your logger with this.options
* @param {string} o.name - name of your project, will appear at the top of log files
* @param {string} o.timestamp - timestamp format
* @param {boolean} o.logToFile - log everything to a file?
* @param {boolean} o.splitFile - split the log file into separate stdout and stderr files?
* @param {number} o.header - include header in log files?
* @param {number} o.maxAge - number of days to keep old log files (-1 to delete all)
* @param {boolean} o.daily - make a new file at the start of every day?
* @param {boolean} o.keepSilent - hide startup messages from the logger?
* @param {boolean} o.debug - log debug messages?
* @param {boolean} o.directory - log files directory
* @param {object} o.levels - object of custom levels, see [docs](https://logger.eartharoid.me/customisation/#log-levels) for help
*/
constructor(o) {
this.options = defaults;
this.ext = [];
if (typeof o === 'object') this.options = merge(this.options, o);
for (let level in this.options.levels) {
this[level] = (text, colour = [], ...args) => {
let l = this.options.levels[level];
if (!l.type) l.type = 'info';
l.type = l.type.trim().toLowerCase();
if ((l.type === 'debug' && this.options.debug === false)) return;
if (typeof text !== 'string') {
try {
if (text instanceof Error) text = text.stack; // stringify errors
else if (typeof text === 'object') text = JSON.stringify(text, null, 2); // stringify objects
else text = String(text);
} catch {
text = String(text);
}
}
if (!(colour instanceof Array)) {
args.unshift(colour);
colour = [];
}
let fg = colour[0], // foreground colour value
bg = colour[1], // background colour value
fgt = 'colours', // foreground colour level
bgt = 'colours'; // background colour level
if (bg) {
bgt = bgColourType(bg);
if (bgt === 'colours' && !bg.startsWith('bg'))
bg = 'bg' + bg[0].toUpperCase() + bg.substring(1); // convert FG colour name to BG
else if (bgt === 'CODE') {
bgt = 'colours';
if (bg[1] !== '!') bg = '&!' + bg[1]; // convert FG code to BG
if (!codes.colours[bg]) throw new LoggerError('Unknown colour code');
bg = codes.colours[bg]; // get leeks colour name
} else if (bgt === 'rgb') bg = bg.replace(' ', '').split(','); // convert to array
}
if (fg) fgt = fgColourType(fg);
if (fgt === 'CODE') {
fgt = 'colours';
if (!codes.colours[fg]) throw new LoggerError('Unknown colour code');
fg = codes.colours[fg]; // get leeks colour name
} else if (fgt === 'rgb') fg = fg.replace(' ', '').split(','); // convert to array
let bgf = !bg
? text : bgt === 'colours'
? leeks[bgt][bg](text) : leeks[bgt](bg, text);
if (!fg) text = bgf;
else text = fgt === 'colours'
? leeks[fgt][fg](bgf) : leeks[fgt](fg, bgf);
let format = replaceCodes(typeof l.format === 'function' ? l.format() : l.format)
.replace(/{+ ?timestamp ?}+/gmi, this.timestamp())
.replace(/{+ ?text ?}+/gmi, text);
console[l.type](leeks.supportsColour ? format : strip(format), ...args);
if (this.options.logToFile && level !== '_logger')
this.writeLine(l.type, format, ...args); // append line to log file
};
}
this.verbose(`Initialising leekslazylogger (v${pkg.version})`);
if (this.options.logToFile === true) this.prepareFile();
else this.verbose('Logging to file is &4disabled');
let custom = this.options.levels.length - defaults.levels.length;
if (this.options.levels.length > defaults.levels.length) this.verbose(`Initialised with ${custom} custom log ${plural('level', custom)}`);
}
/**
* @description Returns timestamp
* @returns {string} timestamp
*/
timestamp() {
return typeof this.options.timestamp === 'function'
? this.options.timestamp()
: dtf(this.options.timestamp);
}
/**
* @description Format a string to add colour with '&' short-codes
* @param {string} String to format
* @returns {string} Formatted text
*/
static format(str) {
return replaceCodes(str);
}
/**
* @description Format a string to add colour with '&' short-codes
* @param {string} String to format
* @returns {string} Formatted text
*/
static f(str) {
return replaceCodes(str);
}
verbose(text) {
if (!this.options.keepSilent) return this._logger(Logger.f(text));
}
/** delete any log files older than this.options.maxAge days */
clearOldFiles(callback) {
const one_day = 1000 * 60 * 60 * 24;
const files = fs.readdirSync(resolve(this.options.directory))
.filter(file => file.endsWith('.log'));
let logs = 0;
let today = new Date();
for (const file of files) {
let lastMod = new Date(fs.statSync(join(this.options.directory, `/${file}`)).mtime);
if (Math.floor((today - lastMod) / one_day) > this.options.maxAge) { // check
fs.unlinkSync(join(this.options.directory, `/${file}`)); // delete
logs++;
}
}
callback(logs);
}
get baseFileName () {
return this.options.daily
? dtf('YYYY-MM-DD')
: dtf('YYYY-MM-DD-HH-mm-ss');
}
get stdoutPath() {
return this.options.splitFile
? join(this.options.directory, `/${this.baseFileName}-stdout.log`)
: join(this.options.directory, `/${this.baseFileName}.log`);
}
get stderrPath() {
return this.options.splitFile
? join(this.options.directory, `/${this.baseFileName}-stderr.log`)
: join(this.options.directory, `/${this.baseFileName}.log`);
}
/** Append the header to the log file, creating the file if it doesn't already exist */
prepareFile() {
this.verbose(`Preparing log ${plural('file', this.options.splitFile ? 2 : 1)}`);
if (!fs.existsSync(resolve(this.options.directory))) { // create logs folder if it doesn't exist
fs.mkdirSync(resolve(this.options.directory));
this.verbose('No logs directory found, creating one for you');
} else {
this.clearOldFiles(logs => {
if (logs >= 1) this.verbose(`Deleted ${logs} old log ${plural('file', logs)}`);
}); // delete old files
}
this.today = dtf('YYYY-MM-DD');
if (this.stdout) this.stdout.end();
if (this.stderr) this.stderr.end();
this.stdout = fs.createWriteStream(this.stdoutPath, {
flags: 'a', // append to end instead of overwriting
});
this.stderr = fs.createWriteStream(this.stderrPath, {
flags: 'a', // append to end instead of overwriting
});
this.file = new Console({
stdout: this.stdout,
stderr: this.stderr
});
let header = require('./header')(this.options);
this.file.log(header);
if (this.options.splitFile)
this.file.error(header);
}
/** Append a line to the log file */
writeLine(type, str, ...args) {
const needsNewFile = this.today !== dtf('YYYY-MM-DD');
if (this.options.daily && needsNewFile) this.prepareFile();
if (!this.options.splitFile) type = 'log';
this.file[type](strip(str), ...args);
}
/**
* @description Register a new extension
* @param {string} name - the extension name, used for storing options
* @param {Object} o - options for this extension (this.options[name])
*/
register(name, o) {
if (typeof o === 'object') this.options[name] = o;
if (this.ext.includes(name)) return;
this.verbose(`${capitalise(name)} extension enabled`);
this.ext.push(name);
}
}
module.exports = Logger;