Skip to content

Instantly share code, notes, and snippets.

@exbotanical
Last active July 7, 2020 23:57
Show Gist options
  • Save exbotanical/62734df7066aff199344626da8236559 to your computer and use it in GitHub Desktop.
Save exbotanical/62734df7066aff199344626da8236559 to your computer and use it in GitHub Desktop.
A logger module with a configurable prototype for dynamic output formatting (including colored).
#!/usr/bin/env node
/**
* @param {Object} config A configurations object for method mappings and options thereof.
* @summary A logger module with a configurable prototype for dynamic output formatting.
* @description This logger module enables the user to instantiate a customized logging utility
* by passing in a configurations object. This configurations object is processed by the
* constructor, wherein values then serve as mappings, the methods and properties of which are dynamically applied.
* Ergo, this module allows the extension and augmentation of the `Console` object to create indeterminate, arbitrary
* methods.
* @extends Console This module extends the native `Console` object's `warn`, `info`, `error` and `log` methods,
* contingent on those methods which the user provides in the aforementioned configurations object.
* @author Matthew T Zito (goldmund)
* @license MIT
*/
class Logger {
constructor(config) {
// standard color dict for extended options
const _colorDict = {
reset : "\x1b[0m",
bright : "\x1b[1m",
dim : "\x1b[2m",
underscore : "\x1b[4m",
blink : "\x1b[5m",
reverse : "\x1b[7m",
hidden : "\x1b[8m",
fgblack : "\x1b[30m",
fgred : "\x1b[31m",
fggreen : "\x1b[32m",
fgyellow : "\x1b[33m",
fgblue : "\x1b[34m",
fgmagenta : "\x1b[35m",
fgcyan : "\x1b[36m",
fgwhite : "\x1b[37m",
bgblack : "\x1b[40m",
bgred : "\x1b[41m",
bggreen : "\x1b[42m",
bgyellow : "\x1b[43m",
bgblue : "\x1b[44m",
bgmagenta : "\x1b[45m",
bgcyan : "\x1b[46m",
bgwhite : "\x1b[47m",
}
// store configuration method values here; we'll be reusing them
const providedMethods = Object.values(config.methods);
/*
we can destructure options *and* apply `toLowerCase` in-line by wrapping the entire assignment in an IIFE
any expressions can be evaluated in this manner and mapped to target destructured val(s) like so:
*/
const { bodyColorSanitized, whiteSpace } = (({ bodyColor, whiteSpace }) => ({ bodyColorSanitized: bodyColor.toLowerCase(), whiteSpace }))(config.options);
// loop through each provided method and dynamically assign to class instance (`this`)
providedMethods.forEach(method => {
// mutate color val strings to prevent case sensitivity
const color = method.color.toLowerCase();
// if provided method is extant in native source, extend props of console obj; else, apply to `log`
let persistentReference = console[method.name] || console.log;
// extend base log functionality to each config-specified method
this[method.name] = function () {
// collate all given args into a 'real' array
let args = Array.prototype.slice.call(arguments);
// map ea. arg so as to exact type-contingent handling thereon
let mutatedArg = args.map(function (arg) {
// hoist memory reference to store polymorphic arg
let str;
let argType = typeof arg;
// case null, simulate `null`
if (arg === null) {
str = "null";
}
// case undefined, simulate unreturned expression
else if (arg === undefined) {
str = "";
}
// case object
else if (!arg.toString || arg.toString() === "[object Object]") {
str = JSON.stringify(arg, null, whiteSpace); // OPTIONAL prettify format
}
// case string
else if (argType === "string") {
str = `"${arg.toString()}"`;
}
// et al
else {
str = arg.toString();
}
return str;
}).join(", ");
// if we've arguments, we do want to format them, yes?
if (args.length) {
/*
Now, we structure the actual format of our logs...
Note: appending the reset hex is imperative here given we've overridden color outputs
*/
args = [
_colorDict[color] +
`${method.label ? method.label : ""}` +
`${method.delimiter ? ` ${method.delimiter} ` : ""}` +
`${bodyColorSanitized ? _colorDict[bodyColorSanitized] : _colorDict.reset}` +
mutatedArg
].concat(method.suffix ? method.suffix : "", _colorDict.reset);
}
// extend and invoke with formatted args
persistentReference.apply(null, args);
};
}, this);
}
}
module.exports = Logger;
/* NOTES */
// Using Symbols
log.levels = {
DEBUG: Symbol("debug"),
INFO: Symbol("info"),
WARN: Symbol("warn"),
};
log(log.levels.DEBUG, "debug message");
log(log.levels.INFO, "info message");
// Using Maps
const logger = {
warn: (...args) => [...args],
error: () => console.log("error")
}
const logMap = new Map();
logMap.set(logger.warn, "warn method")
logMap.set(logger.error, "error method")
const getDescription = _funcName => logMap.get(_funcName);
console.log(getDescription(logger.warn));
// /* Example Config */
let config = {
methods: {
info: {
name: "info",
color: "FgYellow",
label: "[INFO]",
delimiter: "-"
},
warn: {
name: "warn",
color: "FgMagenta",
label: "[WARN]",
delimiter: "-"
},
error: {
name: "error",
color: "FgRed",
label: "[ERROR]",
delimiter: "-"
},
success: {
name: "success",
color: "FgGreen",
label: "[ACK]",
delimiter: "-"
},
},
options: {
bodyColor: "FgYellow",
replacer: null,
whiteSpace: 0
}
};
// /* Usage */
const dowork = (work) => work + 5;
const words = ["no", "fail"];
const wordsAgain = ["yes", "success"];
const logger = new Logger(config);
logger.warn({ alert: true, event: "ready" });
logger.error(words);
logger.success(wordsAgain);
logger.info(dowork(17));

Usage: Logger Module, Vivisector.js

This module allows the creation of a customized logger that can be utilized in lieu of Node's native Console methods. In fact, this module - in part - extends them.

The Logger accepts as input a configurations object. It expects the config to have such a structure:

* Example Config */
let config = {
    methods: {
        info: {
            name: "info",
            color: "FgYellow",
            label: "[INFO]",
            delimiter: "-",
            suffix: ""
        },
        warn: {
            name: "warn",
            color: "FgMagenta",
            label: "[WARN]",
            delimiter: "-"
            suffix: ""
        },
        error: {
            name: "error",
            color: "FgRed",
            label: "[ERROR]",
            delimiter: "-"
            suffix: "\n"
        },
        success: {
            name: "success",
            color: "FgGreen",
            label: "[ACK]",
            delimiter: "-"
            suffix: ""
        }
    },
    options: {
        bodyColor: "FgYellow",
        replacer: null,
        whiteSpace: 0
    }
}

This is my personal configuration. In addition to extending the native info, warn, and error methods, I have also implemented a custom success method. In the config, I have specified certain options for each method which afford a considerably granular control-scope. For instance, here I can set a prefix for the specific logger method, a delimiter with which to separate said prefix from the actual logged data, as well as colors for the prefix-delimiter. Note that you can set the label or suffix properties to a string (like I did with the labels here), or you can use an expression e.g. new Date():

...
yourMethod: {
            name: "test",
            color: "FgGreen",
            label: new Date(),
            delimiter: "- [+]",
            suffix: "\n"
        }
...

let logger = new Logger(yourConfig);
logger.test("this is a test");
logger.test("this is a second test");
// Fri Jun 26 2020 08:08:15 GMT-0700 (Pacific Daylight Time) - [+] "this is a test" 
//
// Fri Jun 26 2020 08:08:15 GMT-0700 (Pacific Daylight Time) - [+] "this is a second test" 

The logger will actually resolve objects using JSON.stringify. As such, some of the options of the config correlate specifically to the format of object output. To colorize the logged data, specify the desired hex value key as options.bodyColor. If you want to prettify/format the output, consider entering as the options.whiteSpace value a whole integer greater than 0. Furthermore, the replacer can be set to filter the stringified object.

To configure the logger, add a method to the config:

const yourConfig = {
    aliasMethodName: {
            name: "yourMethod",
            color: "colorCaseDoesntMatter",
            label: "[method prefix]",
            delimiter: "-->",
            suffix: ""
        },
        ...
}

let logger = new Logger(yourConfig);
logger.yourMethod("hello, world");
// [method prefix] --> "hello, world"
  • aliasMethodName: Object key, arbitrary label for readability. I recommend matching these to name.
  • name: String representing allable method name.
  • color: Case-insensitive string representing color of prefix + delimiter. Key which maps to hex value.
  • label String or expression value which will resolve as prefix/label for given log method. Null or empty string if none.
  • delimiter: String with which to delimit label/prefix from logged data. Null or empty string if none.
  • suffix: String or expression value which will be concatenated to the end of each log. This is particularly useful if you want to separate logs with newlines, as demonstrated in the error method options in my config. Null or empty string if none.

Now, for the options:

const yourConfig = {
    ...
    options: {
        bodyColor: "FgYellow",
        replacer: null,
        whiteSpace: 0
    }
}
  • bodyColor: Color of logged data, applies to all methods. Key which maps to hex value. String, case-insensitive.
  • replacer: Function which alters the behavior of the stringification process, OR an array comprised of elements of types String and/or Number which serves as an 'allowlist' for filtering the properties of the value object to be included in the JSON string. If this value is null or not provided, all properties of the object will be included in the resulting string output.
  • whiteSpace: String or Number object used to insert white space into the output JSON string for readability purposes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment