From fbe6efcf240dd516023e57a64995befb2c3a0cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 24 Jun 2026 11:37:52 +0200 Subject: [PATCH 1/2] Preserve message and name when logging Error instances JSON.stringify only walks enumerable own properties, so Error instances (whose message, name and stack are non-enumerable) serialize to {} or to a partial object when subclasses add own props. This regressed downstream logs after consumers picked up AWS SDK v3, whose errors are real Error subclasses (vs. SDK v2 which set message/code as enumerable own props). Pass a JSON replacer in SimpleLogger.safeStringify that detects Error values and returns a plain object with message and name alongside any own enumerable props. Stack is intentionally omitted to keep log lines compact and match prior behavior. Add unit tests covering plain Error, Error subclasses with own props, nested Errors, non-Error passthrough, and absence of stack. Issue: WRLGS-19 --- lib/SimpleLogger.js | 23 +++++++++++- tests/unit/SimpleLogger.js | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/unit/SimpleLogger.js diff --git a/lib/SimpleLogger.js b/lib/SimpleLogger.js index 46e3e86..265ed4a 100644 --- a/lib/SimpleLogger.js +++ b/lib/SimpleLogger.js @@ -3,6 +3,25 @@ const safeJSONStringify = require('safe-json-stringify'); const fastJSONStringify = require('fast-safe-stringify'); const LogLevel = require('./LogLevel'); +/* +* JSON.stringify (and fast-safe-stringify) only serialize enumerable own +* properties, so Error instances (whose `message` and `name` are +* non-enumerable) serialize to `{}`, or to a partial object when subclasses +* add own props like `code`/`$metadata`). This replacer flattens any Error +* value to a plain object that preserves `message` and `name` alongside any +* own enumerable props. +*/ +function errorReplacer(_key, value) { + if (value instanceof Error) { + return { + ...value, + message: value.message, + name: value.name, + }; + } + return value; +} + /* * This function safely stringifies JSON. If an exception occcurs (due to * circular references, exceptions thrown from object getters etc.), the module @@ -15,13 +34,13 @@ function safeStringify(obj) { let str; try { // Try to stringify the object (fast version) - str = fastJSONStringify(obj); + str = fastJSONStringify(obj, errorReplacer); // eslint-disable-next-line no-unused-vars } catch (e) { // fallback to remove circular object references or other exceptions // eslint-disable-next-line no-param-reassign obj.unsafeJSON = true; - return safeJSONStringify(obj); + return safeJSONStringify(obj, errorReplacer); } return str; } diff --git a/tests/unit/SimpleLogger.js b/tests/unit/SimpleLogger.js new file mode 100644 index 0000000..5ce9a8d --- /dev/null +++ b/tests/unit/SimpleLogger.js @@ -0,0 +1,75 @@ +const assert = require('assert'); +const { PassThrough } = require('stream'); + +const SimpleLogger = require('../../lib/SimpleLogger'); + +function captureLog(fn) { + const pass = new PassThrough(); + const records = []; + pass.on('data', data => records.push(data.toString())); + const logger = new SimpleLogger('test', [{ level: 'trace', stream: pass }]); + fn(logger); + return records.map(r => JSON.parse(r.trim())); +} + +describe('SimpleLogger Error serialization', () => { + it('serializes a plain Error with its message and name', () => { + const [entry] = captureLog(log => { + log.error({ error: new Error('boom') }, 'plain Error'); + }); + + assert.strictEqual(entry.error.message, 'boom'); + assert.strictEqual(entry.error.name, 'Error'); + }); + + it('preserves own enumerable props on Error subclasses', () => { + class S3ServiceException extends Error { + constructor(opts) { + super(opts.message); + this.name = opts.name; + } + } + const err = new S3ServiceException({ + name: 'NoSuchBucket', + message: 'The specified bucket does not exist', + }); + err.$metadata = { httpStatusCode: 404 }; + + const [entry] = captureLog(log => { + log.error({ error: err }, 'sdk v3 style'); + }); + + assert.strictEqual(entry.error.message, 'The specified bucket does not exist'); + assert.strictEqual(entry.error.name, 'NoSuchBucket'); + assert.deepStrictEqual(entry.error.$metadata, { httpStatusCode: 404 }); + }); + + it('does not include the stack', () => { + const [entry] = captureLog(log => { + log.error({ error: new Error('boom') }, 'no stack'); + }); + + assert.strictEqual(entry.error.stack, undefined); + }); + + it('leaves non-Error fields untouched', () => { + const [entry] = captureLog(log => { + log.info({ foo: 1, nested: { bar: 'baz' } }, 'plain object'); + }); + + assert.strictEqual(entry.foo, 1); + assert.deepStrictEqual(entry.nested, { bar: 'baz' }); + }); + + it('serializes Errors nested inside other fields', () => { + const [entry] = captureLog(log => { + log.error({ + // eslint-disable-next-line camelcase + healthStatus: { aws_location: { error: new Error('down') } }, + }, 'nested'); + }); + + assert.strictEqual(entry.healthStatus.aws_location.error.message, 'down'); + assert.strictEqual(entry.healthStatus.aws_location.error.name, 'Error'); + }); +}); From a7bbb5917a08b035d3763b24b070d517483d6982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Wed, 24 Jun 2026 15:12:13 +0200 Subject: [PATCH 2/2] Bump package.json to 8.2.4 Issue: WRLGS-19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0748614..5b2bfce 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "engines": { "node": ">=20" }, - "version": "8.2.3", + "version": "8.2.4", "description": "An efficient raw JSON logging library aimed at micro-services architectures.", "main": "index.js", "scripts": {