From 97442c816f3d5bad9a83b6c2a1c16744e4874bed Mon Sep 17 00:00:00 2001 From: Erika Yao <71943596+erikayao93@users.noreply.github.com> Date: Fri, 30 Jun 2023 11:54:33 -0500 Subject: [PATCH] feat(logger): Support for external observability providers (#1511) * Updated formatAttributes for additional parameters and LogItem return type * Updated the unit tests to pass with new formatter * Updated Powertool named objects to Powertools * Updated tests to match new naming consistency * Updated for tests for new naming consistency * Updated formatter for new design decisions * Update Logger for ephemeral attributes * Update bringYourOwnFormatter documentation to match new formatter --------- Co-authored-by: erikayao93 --- .../logger/bringYourOwnFormatterClass.ts | 13 +- packages/logger/src/Logger.ts | 26 +- packages/logger/src/formatter/LogFormatter.ts | 9 +- .../src/formatter/LogFormatterInterface.ts | 9 +- ...Formatter.ts => PowertoolsLogFormatter.ts} | 25 +- packages/logger/src/formatter/index.ts | 2 +- packages/logger/src/log/LogItem.ts | 10 +- .../{PowertoolLog.ts => PowertoolsLog.ts} | 4 +- packages/logger/src/types/formats/index.ts | 2 +- packages/logger/tests/unit/Logger.test.ts | 34 +- .../formatter/PowertoolLogFormatter.test.ts | 369 -------------- .../formatter/PowertoolsLogFormatter.test.ts | 462 ++++++++++++++++++ packages/logger/tests/unit/helpers.test.ts | 16 +- .../tests/unit/middleware/middy.test.ts | 6 +- 14 files changed, 553 insertions(+), 434 deletions(-) rename packages/logger/src/formatter/{PowertoolLogFormatter.ts => PowertoolsLogFormatter.ts} (58%) rename packages/logger/src/types/formats/{PowertoolLog.ts => PowertoolsLog.ts} (96%) delete mode 100644 packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts create mode 100644 packages/logger/tests/unit/formatter/PowertoolsLogFormatter.test.ts diff --git a/docs/snippets/logger/bringYourOwnFormatterClass.ts b/docs/snippets/logger/bringYourOwnFormatterClass.ts index ab7e207cb9..9d189b2b8b 100644 --- a/docs/snippets/logger/bringYourOwnFormatterClass.ts +++ b/docs/snippets/logger/bringYourOwnFormatterClass.ts @@ -3,13 +3,17 @@ import { LogAttributes, UnformattedAttributes, } from '@aws-lambda-powertools/logger/lib/types'; +import { LogItem } from '@aws-lambda-powertools/logger/lib/log'; // Replace this line with your own type type MyCompanyLog = LogAttributes; class MyCompanyLogFormatter extends LogFormatter { - public formatAttributes(attributes: UnformattedAttributes): MyCompanyLog { - return { + public formatAttributes( + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem { + const baseAttributes: MyCompanyLog = { message: attributes.message, service: attributes.serviceName, environment: attributes.environment, @@ -31,6 +35,11 @@ class MyCompanyLogFormatter extends LogFormatter { sampleRateValue: attributes.sampleRateValue, }, }; + + const logItem = new LogItem({ attributes: baseAttributes }); + logItem.addAttributes(additionalLogAttributes); // add any attributes not explicitly defined + + return logItem; } } diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 7f7338bc4d..5446e866de 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -3,7 +3,7 @@ import { Console } from 'node:console'; import { format } from 'node:util'; import type { Context, Handler } from 'aws-lambda'; import { Utility } from '@aws-lambda-powertools/commons'; -import { LogFormatterInterface, PowertoolLogFormatter } from './formatter'; +import { LogFormatterInterface, PowertoolsLogFormatter } from './formatter'; import { LogItem } from './log'; import merge from 'lodash.merge'; import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; @@ -653,16 +653,13 @@ class Logger extends Utility implements ClassThatLogs { this.getPowertoolLogData() ); - const logItem = new LogItem({ - baseAttributes: this.getLogFormatter().formatAttributes( - unformattedBaseAttributes - ), - persistentAttributes: this.getPersistentLogAttributes(), - }); - - // Add ephemeral attributes + let additionalLogAttributes: LogAttributes = {}; + additionalLogAttributes = merge( + additionalLogAttributes, + this.getPersistentLogAttributes() + ); if (typeof input !== 'string') { - logItem.addAttributes(input); + additionalLogAttributes = merge(additionalLogAttributes, input); } extraInput.forEach((item: Error | LogAttributes | string) => { const attributes: LogAttributes = @@ -672,9 +669,14 @@ class Logger extends Utility implements ClassThatLogs { ? { extra: item } : item; - logItem.addAttributes(attributes); + additionalLogAttributes = merge(additionalLogAttributes, attributes); }); + const logItem = this.getLogFormatter().formatAttributes( + unformattedBaseAttributes, + additionalLogAttributes + ); + return logItem; } @@ -955,7 +957,7 @@ class Logger extends Utility implements ClassThatLogs { * @returns {void} */ private setLogFormatter(logFormatter?: LogFormatterInterface): void { - this.logFormatter = logFormatter || new PowertoolLogFormatter(); + this.logFormatter = logFormatter || new PowertoolsLogFormatter(); } /** diff --git a/packages/logger/src/formatter/LogFormatter.ts b/packages/logger/src/formatter/LogFormatter.ts index 0a00fe55f8..f89591eabe 100644 --- a/packages/logger/src/formatter/LogFormatter.ts +++ b/packages/logger/src/formatter/LogFormatter.ts @@ -1,5 +1,6 @@ import { LogFormatterInterface } from '.'; import { LogAttributes, UnformattedAttributes } from '../types'; +import { LogItem } from '../log'; /** * Typeguard to monkey patch Error to add a cause property. @@ -29,11 +30,13 @@ abstract class LogFormatter implements LogFormatterInterface { * It formats key-value pairs of log attributes. * * @param {UnformattedAttributes} attributes - * @returns {LogAttributes} + * @param {LogAttributes} additionalLogAttributes + * @returns {LogItem} */ public abstract formatAttributes( - attributes: UnformattedAttributes - ): LogAttributes; + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem; /** * It formats a given Error parameter. diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts index b6a771b84e..0fe1dd9909 100644 --- a/packages/logger/src/formatter/LogFormatterInterface.ts +++ b/packages/logger/src/formatter/LogFormatterInterface.ts @@ -1,4 +1,5 @@ import { LogAttributes, UnformattedAttributes } from '../types'; +import { LogItem } from '../log'; /** * @interface @@ -8,9 +9,13 @@ interface LogFormatterInterface { * It formats key-value pairs of log attributes. * * @param {UnformattedAttributes} attributes - * @returns {PowertoolLog} + * @param {LogAttributes} additionalLogAttributes + * @returns {LogItem} */ - formatAttributes(attributes: UnformattedAttributes): LogAttributes; + formatAttributes( + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem; /** * It formats a given Error parameter. diff --git a/packages/logger/src/formatter/PowertoolLogFormatter.ts b/packages/logger/src/formatter/PowertoolsLogFormatter.ts similarity index 58% rename from packages/logger/src/formatter/PowertoolLogFormatter.ts rename to packages/logger/src/formatter/PowertoolsLogFormatter.ts index 0fa77928e2..6a599d6eb8 100644 --- a/packages/logger/src/formatter/PowertoolLogFormatter.ts +++ b/packages/logger/src/formatter/PowertoolsLogFormatter.ts @@ -1,6 +1,7 @@ import { LogFormatter } from '.'; -import { UnformattedAttributes } from '../types'; -import { PowertoolLog } from '../types/formats'; +import { LogAttributes, UnformattedAttributes } from '../types'; +import { PowertoolsLog } from '../types/formats'; +import { LogItem } from '../log'; /** * This class is used to transform a set of log key-value pairs @@ -9,15 +10,19 @@ import { PowertoolLog } from '../types/formats'; * @class * @extends {LogFormatter} */ -class PowertoolLogFormatter extends LogFormatter { +class PowertoolsLogFormatter extends LogFormatter { /** * It formats key-value pairs of log attributes. * * @param {UnformattedAttributes} attributes - * @returns {PowertoolLog} + * @param {LogAttributes} additionalLogAttributes + * @returns {PowertoolsLog} */ - public formatAttributes(attributes: UnformattedAttributes): PowertoolLog { - return { + public formatAttributes( + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem { + const baseAttributes: PowertoolsLog = { cold_start: attributes.lambdaContext?.coldStart, function_arn: attributes.lambdaContext?.invokedFunctionArn, function_memory_size: attributes.lambdaContext?.memoryLimitInMB, @@ -30,7 +35,13 @@ class PowertoolLogFormatter extends LogFormatter { timestamp: this.formatTimestamp(attributes.timestamp), xray_trace_id: attributes.xRayTraceId, }; + + const powertoolLogItem = new LogItem({ attributes: baseAttributes }); + + powertoolLogItem.addAttributes(additionalLogAttributes); + + return powertoolLogItem; } } -export { PowertoolLogFormatter }; +export { PowertoolsLogFormatter }; diff --git a/packages/logger/src/formatter/index.ts b/packages/logger/src/formatter/index.ts index 1f7f14d2ab..ef5d7b16d8 100644 --- a/packages/logger/src/formatter/index.ts +++ b/packages/logger/src/formatter/index.ts @@ -1,3 +1,3 @@ export * from './LogFormatter'; export * from './LogFormatterInterface'; -export * from './PowertoolLogFormatter'; +export * from './PowertoolsLogFormatter'; diff --git a/packages/logger/src/log/LogItem.ts b/packages/logger/src/log/LogItem.ts index 1c562ec171..00a66ec786 100644 --- a/packages/logger/src/log/LogItem.ts +++ b/packages/logger/src/log/LogItem.ts @@ -5,16 +5,12 @@ import { LogAttributes } from '../types'; class LogItem implements LogItemInterface { private attributes: LogAttributes = {}; - public constructor(params: { - baseAttributes: LogAttributes; - persistentAttributes: LogAttributes; - }) { + public constructor(params: { attributes: LogAttributes }) { // Add attributes in the log item in this order: // - Base attributes supported by the Powertool by default - // - Persistent attributes provided by developer, not formatted + // - Persistent attributes provided by developer, not formatted (done later) // - Ephemeral attributes provided as parameters for a single log item (done later) - this.addAttributes(params.baseAttributes); - this.addAttributes(params.persistentAttributes); + this.addAttributes(params.attributes); } public addAttributes(attributes: LogAttributes): LogItem { diff --git a/packages/logger/src/types/formats/PowertoolLog.ts b/packages/logger/src/types/formats/PowertoolsLog.ts similarity index 96% rename from packages/logger/src/types/formats/PowertoolLog.ts rename to packages/logger/src/types/formats/PowertoolsLog.ts index add42d298c..fa360fef59 100644 --- a/packages/logger/src/types/formats/PowertoolLog.ts +++ b/packages/logger/src/types/formats/PowertoolsLog.ts @@ -1,6 +1,6 @@ import type { LogAttributes, LogLevel } from '..'; -type PowertoolLog = LogAttributes & { +type PowertoolsLog = LogAttributes & { /** * timestamp * @@ -90,4 +90,4 @@ type PowertoolLog = LogAttributes & { lambda_request_id?: string; }; -export type { PowertoolLog }; +export type { PowertoolsLog }; diff --git a/packages/logger/src/types/formats/index.ts b/packages/logger/src/types/formats/index.ts index 5462610cd6..5a828a385f 100644 --- a/packages/logger/src/types/formats/index.ts +++ b/packages/logger/src/types/formats/index.ts @@ -1 +1 @@ -export * from './PowertoolLog'; +export * from './PowertoolsLog'; diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index 1c0d1e8582..b9a88e73eb 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -10,7 +10,7 @@ import { } from '@aws-lambda-powertools/commons'; import { createLogger, Logger } from '../../src'; import { EnvironmentVariablesService } from '../../src/config'; -import { PowertoolLogFormatter } from '../../src/formatter'; +import { PowertoolsLogFormatter } from '../../src/formatter'; import { ClassThatLogs, LogJsonIndent, @@ -798,7 +798,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: 0, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1601,7 +1601,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1624,7 +1624,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1647,7 +1647,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1713,7 +1713,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1736,7 +1736,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1762,7 +1762,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1785,7 +1785,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 20, logLevelThresholds: { ...logLevelThresholds, @@ -1829,7 +1829,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1852,7 +1852,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1882,7 +1882,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: INDENTATION, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1923,7 +1923,7 @@ describe('Class: Logger', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: 0, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 8, logLevelThresholds: { ...logLevelThresholds, @@ -1950,7 +1950,7 @@ describe('Class: Logger', () => { test('child logger should have the same logFormatter as its parent', () => { // Prepare - class MyCustomLogFormatter extends PowertoolLogFormatter {} + class MyCustomLogFormatter extends PowertoolsLogFormatter {} const parentLogger = new Logger({ logFormatter: new MyCustomLogFormatter(), }); @@ -1968,7 +1968,7 @@ describe('Class: Logger', () => { test('child logger with custom logFormatter in options should have provided logFormatter', () => { // Prepare - class MyCustomLogFormatter extends PowertoolLogFormatter {} + class MyCustomLogFormatter extends PowertoolsLogFormatter {} const parentLogger = new Logger(); // Act @@ -1979,7 +1979,7 @@ describe('Class: Logger', () => { // Assess expect(parentLogger).toEqual( expect.objectContaining({ - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), }) ); @@ -1992,7 +1992,7 @@ describe('Class: Logger', () => { test('child logger should have exact same attributes as the parent logger created with all non-default options', () => { // Prepare - class MyCustomLogFormatter extends PowertoolLogFormatter {} + class MyCustomLogFormatter extends PowertoolsLogFormatter {} class MyCustomEnvironmentVariablesService extends EnvironmentVariablesService {} const options: ConstructorOptions = { diff --git a/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts b/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts deleted file mode 100644 index 9176ce8d8d..0000000000 --- a/packages/logger/tests/unit/formatter/PowertoolLogFormatter.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -/** - * Test Logger formatter - * - * @group unit/logger/all - */ -import { AssertionError } from 'node:assert'; -import { PowertoolLogFormatter } from '../../../src/formatter'; -import { UnformattedAttributes } from '../../../src/types'; - -describe('Class: PowertoolLogFormatter', () => { - const mockDate = new Date(1466424490000); - const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - beforeEach(() => { - dateSpy.mockClear(); - }); - - describe('Method: formatAttributes', () => { - test('when optional parameters DO NOT have a value set, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - const unformattedAttributes: UnformattedAttributes = { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - xRayTraceId: '1-5759e988-bd862e3fe1be46a994272793', - logLevel: 'WARN', - timestamp: new Date(), - message: 'This is a WARN log', - }; - - // Act - const value = formatter.formatAttributes(unformattedAttributes); - - // Assess - expect(value).toEqual({ - cold_start: undefined, - function_arn: undefined, - function_memory_size: undefined, - function_name: undefined, - function_request_id: undefined, - level: 'WARN', - message: 'This is a WARN log', - sampling_rate: undefined, - service: 'hello-world', - timestamp: '2016-06-20T12:08:10.000Z', - xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', - }); - }); - - test('when optional parameters DO have a value set, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - const unformattedAttributes: UnformattedAttributes = { - sampleRateValue: 0.25, - awsRegion: 'eu-west-1', - environment: 'prod', - serviceName: 'hello-world', - xRayTraceId: '1-5759e988-bd862e3fe1be46a994272793', - logLevel: 'WARN', - timestamp: new Date(), - message: 'This is a WARN log', - error: new Error('Something happened!'), - lambdaContext: { - functionName: 'my-lambda-function', - memoryLimitInMB: 123, - functionVersion: '1.23.3', - coldStart: true, - invokedFunctionArn: - 'arn:aws:lambda:eu-west-1:123456789012:function:Example', - awsRequestId: 'abcdefg123456789', - }, - }; - - // Act - const value = formatter.formatAttributes(unformattedAttributes); - - // Assess - expect(value).toEqual({ - cold_start: true, - function_arn: 'arn:aws:lambda:eu-west-1:123456789012:function:Example', - function_memory_size: 123, - function_name: 'my-lambda-function', - function_request_id: 'abcdefg123456789', - level: 'WARN', - message: 'This is a WARN log', - sampling_rate: 0.25, - service: 'hello-world', - timestamp: '2016-06-20T12:08:10.000Z', - xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', - }); - }); - }); - - describe('Method: formatError', () => { - test('when an error of type Error is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedError = formatter.formatError(new Error('Ouch!')); - expect(formattedError).toEqual({ - location: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+$/ - ), - message: 'Ouch!', - name: 'Error', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type ReferenceError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedReferenceError = formatter.formatError( - new ReferenceError('doesNotExist is not defined') - ); - expect(formattedReferenceError).toEqual({ - location: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+$/ - ), - message: 'doesNotExist is not defined', - name: 'ReferenceError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type AssertionError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedAssertionError = formatter.formatError( - new AssertionError({ - message: 'Expected values to be strictly equal', - actual: 1, - expected: 2, - operator: 'strictEqual', - }) - ); - expect(formattedAssertionError).toEqual({ - location: expect.stringMatching( - /(node:)*internal\/assert\/assertion_error(.js)*:[0-9]+$/ - ), - message: expect.stringMatching(/Expected values to be strictly equal/), - name: 'AssertionError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type RangeError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedRangeError = formatter.formatError( - new RangeError('The argument must be between 10 and 20') - ); - expect(formattedRangeError).toEqual({ - location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), - message: 'The argument must be between 10 and 20', - name: 'RangeError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type ReferenceError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedError = formatter.formatError( - new ReferenceError('foo is not defined') - ); - expect(formattedError).toEqual({ - location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), - message: 'foo is not defined', - name: 'ReferenceError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type SyntaxError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedSyntaxError = formatter.formatError( - new SyntaxError(`Unexpected identifier 'bar'`) - ); - expect(formattedSyntaxError).toEqual({ - location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), - message: `Unexpected identifier 'bar'`, - name: 'SyntaxError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type TypeError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedTypeError = formatter.formatError( - new TypeError(`Cannot read property 'foo' of null`) - ); - expect(formattedTypeError).toEqual({ - location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), - message: expect.stringMatching(/Cannot read propert/), - name: 'TypeError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - test('when an error of type URIError is passed, it returns an object with expected structure and values', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act & Assess - const formattedURIError = formatter.formatError( - new URIError('URI malformed') - ); - expect(formattedURIError).toEqual({ - location: expect.stringMatching(/PowertoolLogFormatter.test.ts:[0-9]+/), - message: 'URI malformed', - name: 'URIError', - stack: expect.stringMatching( - /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ - ), - }); - }); - - 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', () => { - test('it returns a datetime value ISO 8601 compliant', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - - // Act - const timestamp = formatter.formatTimestamp(new Date()); - - // Assess - expect(timestamp).toEqual('2016-06-20T12:08:10.000Z'); - }); - }); - - describe('Method: getCodeLocation', () => { - test('when the stack IS present, it returns a datetime value ISO 8601 compliant', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - const stack = - 'Error: Things keep happening!\n' + - ' at /home/foo/bar/file-that-threw-the-error.ts:22:5\n' + - ' at SomeOther.function (/home/foo/bar/some-file.ts:154:19)'; - - // Act - const errorLocation = formatter.getCodeLocation(stack); - - // Assess - expect(errorLocation).toEqual('/home/foo/bar/some-file.ts:154'); - }); - - test('when the stack IS NOT present, it returns a datetime value ISO 8601 compliant', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - const stack = undefined; - - // Act - const errorLocation = formatter.getCodeLocation(stack); - - // Assess - expect(errorLocation).toEqual(''); - }); - - test('when the stack IS NOT present, it returns a datetime value ISO 8601 compliant', () => { - // Prepare - const formatter = new PowertoolLogFormatter(); - const stack = 'A weird stack trace...'; - - // Act - const errorLocation = formatter.getCodeLocation(stack); - - // Assess - expect(errorLocation).toEqual(''); - }); - }); -}); diff --git a/packages/logger/tests/unit/formatter/PowertoolsLogFormatter.test.ts b/packages/logger/tests/unit/formatter/PowertoolsLogFormatter.test.ts new file mode 100644 index 0000000000..92ce661aab --- /dev/null +++ b/packages/logger/tests/unit/formatter/PowertoolsLogFormatter.test.ts @@ -0,0 +1,462 @@ +/** + * Test Logger formatter + * + * @group unit/logger/all + */ +import { AssertionError, strictEqual } from 'assert'; +import { PowertoolsLogFormatter } from '../../../src/formatter'; +import { LogAttributes, UnformattedAttributes } from '../../../src/types'; + +describe('Class: PowertoolsLogFormatter', () => { + const mockDate = new Date(1466424490000); + const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + + beforeEach(() => { + dateSpy.mockClear(); + }); + + describe('Method: formatAttributes', () => { + test('when optional parameters DO NOT have a value set, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const unformattedAttributes: UnformattedAttributes = { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + xRayTraceId: '1-5759e988-bd862e3fe1be46a994272793', + logLevel: 'WARN', + timestamp: new Date(), + message: 'This is a WARN log', + }; + const additionalLogAttributes: LogAttributes = {}; + + // Act + const value = formatter.formatAttributes( + unformattedAttributes, + additionalLogAttributes + ); + + // Assess + expect(value.getAttributes()).toEqual({ + cold_start: undefined, + function_arn: undefined, + function_memory_size: undefined, + function_name: undefined, + function_request_id: undefined, + level: 'WARN', + message: 'This is a WARN log', + sampling_rate: undefined, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + }); + }); + + test('when optional parameters DO have a value set, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const unformattedAttributes: UnformattedAttributes = { + sampleRateValue: 0.25, + awsRegion: 'eu-west-1', + environment: 'prod', + serviceName: 'hello-world', + xRayTraceId: '1-5759e988-bd862e3fe1be46a994272793', + logLevel: 'WARN', + timestamp: new Date(), + message: 'This is a WARN log', + error: new Error('Something happened!'), + lambdaContext: { + functionName: 'my-lambda-function', + memoryLimitInMB: 123, + functionVersion: '1.23.3', + coldStart: true, + invokedFunctionArn: + 'arn:aws:lambda:eu-west-1:123456789012:function:Example', + awsRequestId: 'abcdefg123456789', + }, + }; + const additionalLogAttributes: LogAttributes = {}; + + // Act + const value = formatter.formatAttributes( + unformattedAttributes, + additionalLogAttributes + ); + + // Assess + expect(value.getAttributes()).toEqual({ + cold_start: true, + function_arn: 'arn:aws:lambda:eu-west-1:123456789012:function:Example', + function_memory_size: 123, + function_name: 'my-lambda-function', + function_request_id: 'abcdefg123456789', + level: 'WARN', + message: 'This is a WARN log', + sampling_rate: 0.25, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + }); + }); + }); + + describe('Method: formatError', () => { + test('when an error of type Error is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + throw new Error('Ouch!'); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(Error); + const formattedError = formatter.formatError(error); + expect(formattedError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+$/ + ), + message: 'Ouch!', + name: 'Error', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + expect(shouldThrow).toThrowError(expect.any(Error)); + }); + + test('when an error of type ReferenceError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + // This is a reference error purposely to test the formatter + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + doesNotExist; + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(Error); + const formattedReferenceError = formatter.formatError(error); + expect(formattedReferenceError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+$/ + ), + message: 'doesNotExist is not defined', + name: 'ReferenceError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + expect(shouldThrow).toThrowError(expect.any(ReferenceError)); + }); + + test('when an error of type AssertionError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + strictEqual(1, 2); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(AssertionError); + const formattedAssertionError = formatter.formatError( + error + ); + expect(formattedAssertionError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+/ + ), + message: expect.stringMatching( + /Expected values to be strictly equal/ + ), + name: 'AssertionError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + expect(shouldThrow).toThrowError(expect.any(AssertionError)); + }); + + test('when an error of type RangeError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + throw new RangeError('The argument must be between 10 and 20'); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(RangeError); + const formattedRangeError = formatter.formatError(error); + expect(formattedRangeError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+/ + ), + message: 'The argument must be between 10 and 20', + name: 'RangeError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + test('when an error of type ReferenceError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + + // Act & Assess + const formattedError = formatter.formatError( + new ReferenceError('foo is not defined') + ); + expect(formattedError).toEqual({ + location: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+/ + ), + message: 'foo is not defined', + name: 'ReferenceError', + stack: expect.stringMatching( + /PowertoolLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + }); + + test('when an error of type SyntaxError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + eval('foo bar'); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(SyntaxError); + const formattedSyntaxError = formatter.formatError( + error + ); + expect(formattedSyntaxError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+/ + ), + message: 'Unexpected identifier', + name: 'SyntaxError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + expect(shouldThrow).toThrowError(expect.any(SyntaxError)); + }); + + test('when an error of type TypeError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + // This is a reference error purposely to test the formatter + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + null.foo(); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // TODO: review message content assertion (see Issue #304) + // Assess + expect(error).toBeInstanceOf(Error); + const formattedTypeError = formatter.formatError(error); + expect(formattedTypeError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+/ + ), + message: expect.stringMatching(/Cannot read propert/), + name: 'TypeError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + expect(shouldThrow).toThrowError(expect.any(TypeError)); + }); + + test('when an error of type URIError is passed, it returns an object with expected structure and values', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const shouldThrow = (): void => { + decodeURIComponent('%'); + }; + + // Act + try { + shouldThrow(); + } catch (error) { + // Assess + expect(error).toBeInstanceOf(URIError); + const formattedURIError = formatter.formatError(error); + expect(formattedURIError).toEqual({ + location: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+/ + ), + message: 'URI malformed', + name: 'URIError', + stack: expect.stringMatching( + /PowertoolsLogFormatter.test.ts:[0-9]+:[0-9]+/ + ), + }); + } + + 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 PowertoolsLogFormatter(); + 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 PowertoolsLogFormatter(); + 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', () => { + test('it returns a datetime value ISO 8601 compliant', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + + // Act + const timestamp = formatter.formatTimestamp(new Date()); + + // Assess + expect(timestamp).toEqual('2016-06-20T12:08:10.000Z'); + }); + }); + + describe('Method: getCodeLocation', () => { + test('when the stack IS present, it returns a datetime value ISO 8601 compliant', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const stack = + 'Error: Things keep happening!\n' + + ' at /home/foo/bar/file-that-threw-the-error.ts:22:5\n' + + ' at SomeOther.function (/home/foo/bar/some-file.ts:154:19)'; + + // Act + const errorLocation = formatter.getCodeLocation(stack); + + // Assess + expect(errorLocation).toEqual('/home/foo/bar/some-file.ts:154'); + }); + + test('when the stack IS NOT present, it returns a datetime value ISO 8601 compliant', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const stack = undefined; + + // Act + const errorLocation = formatter.getCodeLocation(stack); + + // Assess + expect(errorLocation).toEqual(''); + }); + + test('when the stack IS NOT present, it returns a datetime value ISO 8601 compliant', () => { + // Prepare + const formatter = new PowertoolsLogFormatter(); + const stack = 'A weird stack trace...'; + + // Act + const errorLocation = formatter.getCodeLocation(stack); + + // Assess + expect(errorLocation).toEqual(''); + }); + }); + }); +}); diff --git a/packages/logger/tests/unit/helpers.test.ts b/packages/logger/tests/unit/helpers.test.ts index 39597f3be1..5b45837a7e 100644 --- a/packages/logger/tests/unit/helpers.test.ts +++ b/packages/logger/tests/unit/helpers.test.ts @@ -8,7 +8,7 @@ import { ConfigServiceInterface, EnvironmentVariablesService, } from '../../src/config'; -import { LogFormatter, PowertoolLogFormatter } from '../../src/formatter'; +import { LogFormatter, PowertoolsLogFormatter } from '../../src/formatter'; import { ConstructorOptions, LogLevelThresholds } from '../../src/types'; import { createLogger, Logger } from './../../src'; @@ -56,7 +56,7 @@ describe('Helper: createLogger function', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', logLevel: 8, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), }) ); }); @@ -67,7 +67,7 @@ describe('Helper: createLogger function', () => { logLevel: 'WARN', serviceName: 'my-lambda-service', sampleRateValue: 1, - logFormatter: new PowertoolLogFormatter(), + logFormatter: new PowertoolsLogFormatter(), customConfigService: new EnvironmentVariablesService(), persistentLogAttributes: { awsAccountId: '123456789', @@ -87,7 +87,7 @@ describe('Helper: createLogger function', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: 0, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 16, console: expect.any(Console), logLevelThresholds: { @@ -124,7 +124,7 @@ describe('Helper: createLogger function', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: 0, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 12, console: expect.any(Console), logLevelThresholds: { @@ -223,7 +223,7 @@ describe('Helper: createLogger function', () => { envVarsService: expect.any(EnvironmentVariablesService), customConfigService: undefined, logLevel: 20, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), }) ); }); @@ -252,7 +252,7 @@ describe('Helper: createLogger function', () => { envVarsService: expect.any(EnvironmentVariablesService), customConfigService: undefined, logLevel: 16, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), }) ); }); @@ -274,7 +274,7 @@ describe('Helper: createLogger function', () => { envVarsService: expect.any(EnvironmentVariablesService), logEvent: false, logIndentation: 0, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), logLevel: 12, console: expect.any(Console), logLevelThresholds: { diff --git a/packages/logger/tests/unit/middleware/middy.test.ts b/packages/logger/tests/unit/middleware/middy.test.ts index 8a9e09e261..f92670f496 100644 --- a/packages/logger/tests/unit/middleware/middy.test.ts +++ b/packages/logger/tests/unit/middleware/middy.test.ts @@ -15,7 +15,7 @@ import { import { injectLambdaContext } from '../../../src/middleware/middy'; import { Logger } from './../../../src'; import middy from '@middy/core'; -import { PowertoolLogFormatter } from '../../../src/formatter'; +import { PowertoolsLogFormatter } from '../../../src/formatter'; import { Console } from 'console'; import { Context } from 'aws-lambda'; @@ -75,7 +75,7 @@ describe('Middy middleware', () => { envVarsService: expect.any(EnvironmentVariablesService), customConfigService: undefined, logLevel: 8, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), }) ); }); @@ -114,7 +114,7 @@ describe('Middy middleware', () => { envVarsService: expect.any(EnvironmentVariablesService), customConfigService: undefined, logLevel: 8, - logFormatter: expect.any(PowertoolLogFormatter), + logFormatter: expect.any(PowertoolsLogFormatter), console: expect.any(Console), }); expect(logger).toEqual(expectation);