* @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(),
data: data || {},
...(other.length && { other }),
...this.asyncLogObject,
- }, this.jsonReplacer.bind(this));
+ };
+ return JSON.stringify(logPayload, replacer);
}
/**
- * 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;
+ }
}
}