X-Git-Url: http://git.squeep.com/?p=squeep-logger-json-console;a=blobdiff_plain;f=lib%2Flogger.js;fp=lib%2Flogger.js;h=a040bb8ba3f9e759f3003e3af562c6c0fdc7d386;hp=0000000000000000000000000000000000000000;hb=229dafe0003708b9fad190b4c0fc68136efd4f8c;hpb=21506cf49b1d239d780b377651554dc868639477 diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..a040bb8 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,125 @@ +'use strict'; + +const jsonReplacers = require('./json-replacers'); +const dataSanitizers = require('./data-sanitizers'); + +/** + * Log as JSON to stdout/stderr. + */ + +const nop = () => { /**/ }; + + +class Logger { + /** + * Wrap backend calls with payload normalization. + * @param {Object} options + * @param {String} options.nodeId - unique identifier for running instance, usually a uuid + * @param {Object} options.logger + * @param {String} options.logger.ignoreBelowLevel - minimum level to log + */ + constructor(options) { + const logLevels = Object.keys(Logger.nullLogger); + const ignoreBelowLevel = options && options.logger && options.logger.ignoreBelowLevel || 'debug'; + this.backend = console; + this.nodeId = options.nodeId; + this.jsonReplacers = Object.values(jsonReplacers); + this.dataSanitizers = Object.values(dataSanitizers); + + if (!logLevels.includes(ignoreBelowLevel)) { + throw new RangeError(`unrecognized minimum log level '${ignoreBelowLevel}'`); + } + const ignoreLevelIdx = logLevels.indexOf(ignoreBelowLevel); + logLevels.forEach((level) => { + // eslint-disable-next-line security/detect-object-injection + this[level] = (logLevels.indexOf(level) > ignoreLevelIdx) ? + nop : + (...args) => this.backend[level](this.payload(level, ...args)); // eslint-disable-line security/detect-object-injection + }); + } + + + /** + * All the expected Console levels, but do nothing. + * Ordered from highest priority to lowest. + */ + static get nullLogger() { + return { + error: nop, + warn: nop, + info: nop, + log: nop, + debug: nop, + }; + } + + + /** + * Structure all expected log data into JSON string. + * @param {String} level + * @param {String} scope + * @param {String} message + * @param {Object} data + * @param {...any} other + * @returns {String} JSON string + */ + payload(level, scope, message, data, ...other) { + if (this.sanitizationNeeded(data)) { + // Create copy of data so we are not changing anything important. + data = JSON.parse(JSON.stringify(data, this.jsonReplacer.bind(this))); + this.sanitize(data); + } + + const now = new Date(); + return JSON.stringify({ + nodeId: this.nodeId, + timestamp: now.toISOString(), + timestampMs: now.getTime(), + level: level, + scope: scope || '[unknown]', + message: message || '', + data: data || {}, + ...(other.length && { other }), + }, this.jsonReplacer.bind(this)); + } + + + /** + * Determine if data needs sanitizing. + * @param {Object} data + * @returns {Boolean} + */ + sanitizationNeeded(data) { + return this.dataSanitizers.some((sanitizer) => sanitizer(data, false)); + } + + + /** + * Mogrify data. + * @param {Object} data + */ + sanitize(data) { + this.dataSanitizers.forEach((sanitizer) => sanitizer(data)); + } + + + /** + * Convert data into JSON. + * @param {String} _key + * @param {*} value + * @returns {String} serialized value + */ + jsonReplacer(key, value) { + let replaced; + + // Try applying all our replacers, until one does something. + this.jsonReplacers.every((replacer) => { + ({ replaced, value } = replacer(key, value)); + return !replaced; + }); + return value; + } + +} + +module.exports = Logger;