--- /dev/null
+'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;