change replacer function signatures to match that of stringify, detect circular refer...
[squeep-logger-json-console] / lib / logger.js
index d5a0c908dae0f417832cbf0fb6864d3c0f34160c..e11810e1d312b5efa36478b418a5bdf25c27a728 100644 (file)
@@ -90,14 +90,16 @@ class Logger {
    * @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)));
+      data = structuredClone(data, replacer);
       this.sanitize(data);
     }
 
     const now = new Date();
-    return JSON.stringify({
+    const logPayload = {
       ...this.commonObject,
       timestamp: now.toISOString(),
       timestampMs: now.getTime(),
@@ -107,7 +109,8 @@ class Logger {
       data: data || {},
       ...(other.length && { other }),
       ...this.asyncLogObject,
-    }, this.jsonReplacer.bind(this));
+    };
+    return JSON.stringify(logPayload, replacer);
   }
 
 
@@ -131,20 +134,31 @@ class Logger {
 
 
   /**
-   * 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.
    */
-  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) {
+      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)) { // eslint-disable-line security/detect-object-injection
+          return '[Circular]';
+        } else {
+          ancestors.push(value);
+        }
+      }
+
+      loggerReplacers.every((replacer) => {
+        const oldValue = value;
+        value  = replacer(key, value);
+        return oldValue === value;
+      });
+      return value;
+    }
   }
 
 }