Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions lib/SimpleLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/SimpleLogger.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading