From 0ee00d23b8bd89707e7c278d94479e2c8a1ea9f1 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 14 Jul 2023 14:24:42 +0000 Subject: [PATCH] feat(logger): add cause to formatted error --- packages/logger/src/formatter/LogFormatter.ts | 22 +++++++ .../formatter/PowertoolLogFormatter.test.ts | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/packages/logger/src/formatter/LogFormatter.ts b/packages/logger/src/formatter/LogFormatter.ts index dac142a05a..539d0b814d 100644 --- a/packages/logger/src/formatter/LogFormatter.ts +++ b/packages/logger/src/formatter/LogFormatter.ts @@ -1,6 +1,23 @@ import { LogFormatterInterface } from '.'; import { LogAttributes, UnformattedAttributes } from '../types'; +/** + * Typeguard to monkey patch Error to add a cause property. + * + * This is needed because the `cause` property was added in Node 16.x. + * Since we want to be able to format errors in Node 14.x, we need to + * add this property ourselves. We can remove this once we drop support + * for Node 14.x. + * + * @see 1361 + * @see https://nodejs.org/api/errors.html#errors_error_cause + */ +const isErrorWithCause = ( + error: Error +): error is Error & { cause: unknown } => { + return 'cause' in error; +}; + /** * This class defines and implements common methods for the formatting of log attributes. * @@ -31,6 +48,11 @@ abstract class LogFormatter implements LogFormatterInterface { location: this.getCodeLocation(error.stack), message: error.message, stack: error.stack, + cause: isErrorWithCause(error) + ? error.cause instanceof Error + ? this.formatError(error.cause) + : error.cause + : undefined, }; } diff --git a/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts b/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts index 49daebe597..02e1e2a303 100644 --- a/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts +++ b/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts @@ -307,6 +307,71 @@ describe('Class: PowertoolLogFormatter', () => { expect(shouldThrow).toThrowError(expect.any(URIError)); }); + + test('when an error with cause of type Error is formatted, the cause key is included and formatted', () => { + // Prepare + const formatter = new PowertoolLogFormatter(); + class ErrorWithCause extends Error { + public cause?: Error; + public constructor(message: string, options?: { cause: Error }) { + super(message); + this.cause = options?.cause; + } + } + + // Act + const formattedURIError = formatter.formatError( + new ErrorWithCause('foo', { cause: new Error('bar') }) + ); + + // Assess + expect(formattedURIError).toEqual({ + location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), + message: 'foo', + name: 'Error', + stack: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + cause: { + location: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+/ + ), + message: 'bar', + name: 'Error', + stack: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }, + }); + }); + + test('when an error with cause of type other than Error is formatted, the cause key is included as-is', () => { + // Prepare + const formatter = new PowertoolLogFormatter(); + class ErrorWithCause extends Error { + public cause?: unknown; + public constructor(message: string, options?: { cause: unknown }) { + super(message); + this.cause = options?.cause; + } + } + + // Act + const formattedURIError = formatter.formatError( + new ErrorWithCause('foo', { cause: 'bar' }) + ); + + // Assess + expect(formattedURIError).toEqual({ + location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), + message: 'foo', + name: 'Error', + stack: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + cause: 'bar', + }); + }); }); describe('Method: formatTimestamp', () => {