palin
Version:
A beautiful Bristol formatter for development logging
197 lines (166 loc) • 6.6 kB
JavaScript
const util = require('util'); // node's util
const chalk = require('chalk');
const check = require('check-types');
// shortens the file name to exclude any path before the project folder name
// @param path: file path string. e.g. /home/mark/myproj/run.js
// @param rootFolderName: root folder for the project. e.g. "myproj"
// @return the shortened file path string
function truncFilename(path, rootFolderName) {
// bail if a string wasn't provided
if (typeof path !== 'string') {
return path;
}
var index = path.indexOf(rootFolderName);
if (index > 0) {
return path.substring(index + rootFolderName.length + 1);
} else {
return path;
}
}
// retruns the colorized timestamp string
function getTimestampString(date) {
// all this nasty code is faster (~30k ops/sec) than doing "moment(date).format('HH:mm:ss:SSS')" and means 0 dependencies
var hour = '0' + date.getHours();
hour = hour.slice(hour.length - 2);
var minute = '0' + date.getMinutes();
minute = minute.slice(minute.length - 2);
var second = '0' + date.getSeconds();
second = second.slice(second.length - 2);
var ms = '' + date.getMilliseconds();
// https://github.com/MarkHerhold/palin/issues/6
// this is faster than using an actual left-pad algorithm
if (ms.length === 1) {
ms = '00' + ms;
} else if (ms.length === 2) {
ms = '0' + ms;
} // no modifications for 3 or more digits
return chalk.dim(`${hour}:${minute}:${second}:${ms}`);
}
const severityMap = {
error: 'bgRed',
warn: 'bgYellow',
info: 'white',
debug: 'white',
trace: 'white'
};
// returns the colorized text for the given severity level
function getColorSeverity(severity) {
// get the color associated with the severity level
const color = severityMap[severity] || 'white';
return chalk[color].bold(severity.toUpperCase());
}
var prevColor = 0; // keep track of the previous scope color
const colors = [/*'black',*/ 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray'];
var scopeColorMap = {};
function getScopeColor(scope) {
if (scopeColorMap[scope]) {
return scopeColorMap[scope];
} else {
var color = colors[prevColor++ % colors.length];
scopeColorMap[scope] = color;
return color;
}
}
// returns the colorized scope string
function getScopeString(scope) {
return chalk[getScopeColor(scope)].bold(scope);
}
// default indentation string. Lives here so it is not recomputed every time formatter() is called
const defaultIndent = chalk.gray('\n → ');
// the formatter to export
const formatter = function formatter(options, severity, date, elems) {
/*
OPTIONS
*/
const indent = options.indent || defaultIndent;
const objectDepth = options.objectDepth;
const source = options.hasOwnProperty('source') ? options.source : true;
const timestamp = (function () {
if (check.function(options.timestamp)) {
return options.timestamp; // user-provided timestamp generating function
} else if (options.timestamp === false) {
return false; // no timestamp
} else {
return getTimestampString; // default timestamp generating function
}
})();
const rootFolderName = options.rootFolderName;
/*
LOGIC
*/
// the last element is an aggregate object of all of the additional passed in elements
const aggObj = elems[elems.length - 1];
// initial log string
let build = ' ';
// add the date
if (timestamp !== false) {
// otherwise, use the default timestamp generator function
build += ' ' + timestamp(date);
}
build += ' ' + getColorSeverity(severity) + ' ';
// add the component if provided
if (aggObj.scope) {
build += getScopeString(aggObj.scope) + ' ';
delete aggObj.scope;
}
// errors are a special case that we absolutely need to keep track of and log the entire stack
const errors = [];
for (let i = 0; i < elems.length - 1; i++) { // iterate through all elements in the array except the last (obj map of options)
const element = elems[i];
// Attempt to determine an appropriate title given the first element
if (i === 0) {
let elementConsumed = false;
if (check.string(element)) {
// string is obviously the title
build += chalk.blue(element);
elementConsumed = true;
} else if (check.instance(element, Error)) {
// title is the error text representation
build += chalk.blue(element.message || '[no message]');
// also store error stacktrace in the aggregate object
errors.push(element);
elementConsumed = true;
}
// add on the file and line number, which always go after the title, inline
if (aggObj.file && aggObj.line) {
if (source){
aggObj.file = truncFilename(aggObj.file, rootFolderName);
build += chalk.dim(` (${aggObj.file}:${aggObj.line})`);
}
delete aggObj.file;
delete aggObj.line;
}
// do not add element 0 to the 'extra' data section
if (elementConsumed) {
continue;
}
}
// add the element to the errors array if it's an error
if (check.instance(element, Error)) {
errors.push(element);
// the error will be concatinated later so continue to the next element
continue;
}
const objString = '\n' + util.inspect(element, { colors: true, depth: objectDepth, compact: true });
build += objString.replace(/\n/g, indent);
}
if (Object.keys(aggObj).length > 0) {
const objString = '\n' + util.inspect(aggObj, { colors: true, depth: objectDepth, compact: true });
build += objString.replace(/\n/g, indent);
}
// iterate through the top-level object keys looking for Errors as well
for (const o of Object.keys(aggObj)) {
if (check.instance(o, Error)) {
errors.push(o);
}
}
// iterate through all the Error objects and print the stacks
for (const e of errors) {
build += indent + e.stack.replace(/\n/g, indent);
}
return build;
};
module.exports = formatter;
// further exports for testing
formatter._getTimestampString = getTimestampString;
;