const jsonReplacers = require('./json-replacers');
const dataSanitizers = require('./data-sanitizers');
-const nop = () => { /**/ };
+const nop = () => undefined;
class Logger {
/**
- * @typedef {Object} ConsoleLike
- * @property {Function(...):void} error
- * @property {Function(...):void} warn
- * @property {Function(...):void} info
- * @property {Function(...):void} log
- * @property {Function(...):void} debug
+ * @callback LogFn
+ * @param {any} ...args values to log
+ * @returns {void} nothing
*/
/**
- * @typedef {Object} LoggerOptions
- * @property {String} ignoreBelowLevel - minimum level to log, e.g. 'info'
+ * @typedef {object} ConsoleLike
+ * @property {LogFn} error error level
+ * @property {LogFn} warn warn level
+ * @property {LogFn} info info level
+ * @property {LogFn} log log level
+ * @property {LogFn} debug debug level
+ */
+ /**
+ * @typedef AsyncLocalStorage
+ * @property {Function} getStore local storage
+ */
+ /**
+ * @typedef {object} LoggerOptions
+ * @property {string} ignoreBelowLevel minimum level to log, e.g. 'info'
*/
/**
* Wrap backend calls with payload normalization and metadata.
- * @param {LoggerOptions} options
- * @param {Object} commonObject - object to be merged into logged json
- * @param {AsyncLocalStorage} asyncLocalStorage - async storage for an object to be merged into logged json
- * @param {ConsoleLike} backend - default is console
+ * @param {LoggerOptions} options options
+ * @param {object} commonObject object to be merged into logged json
+ * @param {AsyncLocalStorage} asyncLocalStorage async storage for an object to be merged into logged json
+ * @param {ConsoleLike} backend default is console
*/
constructor(options, commonObject, asyncLocalStorage, backend) {
const logLevels = Object.keys(Logger.nullLogger);
/**
* All the expected Console levels, but do nothing.
* Ordered from highest priority to lowest.
+ * @returns {object} no-op backend
*/
static get nullLogger() {
return {
/**
* Default of empty object when no asyncLocalStorage is defined.
* Overridden on instance when asyncLocalStorage is defined.
+ * @returns {object} empty
*/
// eslint-disable-next-line class-methods-use-this
get asyncLogObject() {
/**
* 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
+ * @param {string} level log level
+ * @param {string} scope scope
+ * @param {string} message message
+ * @param {object} data data
+ * @param {...any} other more data merged into payload
+ * @returns {string} JSON string
*/
payload(level, scope, message, data, ...other) {
+ const replacer = this.getReplacer();
+
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)));
+ try {
+ data = structuredClone(data);
+ } catch (e) { // eslint-disable-line no-unused-vars
+ data = JSON.parse(JSON.stringify(data, replacer));
+ }
this.sanitize(data);
}
const now = new Date();
- return JSON.stringify({
+ const logPayload = {
...this.commonObject,
timestamp: now.toISOString(),
timestampMs: now.getTime(),
data: data || {},
...(other.length && { other }),
...this.asyncLogObject,
- }, this.jsonReplacer.bind(this));
+ };
+ return JSON.stringify(logPayload, replacer);
}
/**
* Determine if data needs sanitizing.
- * @param {Object} data
- * @returns {Boolean}
+ * @param {object} data data
+ * @returns {boolean} needs sanitization
*/
sanitizationNeeded(data) {
return this.dataSanitizers.some((sanitizer) => sanitizer(data, false));
/**
* Mogrify data.
- * @param {Object} data
+ * @param {object} data data
*/
sanitize(data) {
this.dataSanitizers.forEach((sanitizer) => sanitizer(data));
/**
- * Convert data into JSON.
- * @param {String} _key
- * @param {*} value
- * @returns {String} serialized value
+ * Return a replacer function which does de-cycling, as well as the rest of our replacers.
+ * @returns {Function} replacer
*/
- 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;
+ getReplacer() {
+ const ancestors = [];
+ const loggerReplacers = this.jsonReplacers;
+ return function cycleReplacer(key, value) {
+ loggerReplacers.every((replacer) => {
+ const oldValue = value;
+ value = replacer(key, value);
+ return oldValue === value;
+ });
+ if (typeof value === 'object' && value !== null) {
+ // 'this' is object where key/value came from
+ while (ancestors.length > 0 && ancestors.at(-1) !== this) {
+ ancestors.pop();
+ }
+ if (ancestors.includes(value)) {
+ return '[Circular]';
+ } else {
+ ancestors.push(value);
+ }
+ }
+
+ return value;
+ };
}
}