From bca82d1d4e94bbf8a53eeaf47603b4c43fe494cf Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 12:34:24 +0600 Subject: [PATCH 01/13] feat: `CustomReplacerFn` type for constructor --- packages/logger/src/types/Logger.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index e662669fe3..00af643eab 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -28,6 +28,11 @@ type InjectLambdaContextOptions = { resetKeys?: boolean; }; +/** + * Custom replacer function for JSON.stringify. + */ +type CustomReplacerFn = (key: string, value: unknown) => void; + type BaseConstructorOptions = { logLevel?: LogLevel; serviceName?: string; @@ -35,6 +40,7 @@ type BaseConstructorOptions = { logFormatter?: LogFormatterInterface; customConfigService?: ConfigServiceInterface; environment?: Environment; + jsonReplacerFn?: CustomReplacerFn; }; type PersistentKeysOption = { @@ -139,4 +145,5 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, + CustomReplacerFn, }; From 55d15c08f879ba226ac915f938365c206e6c69c2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 12:59:59 +0600 Subject: [PATCH 02/13] feat: use custom replacer function if provided --- packages/logger/src/Logger.ts | 19 ++++++++++++++++++- packages/logger/src/types/Logger.ts | 4 +--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 6c0c37e740..fca085a331 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -24,6 +24,7 @@ import type { LogItemMessage, LoggerInterface, PowertoolsLogData, + CustomReplacerFn, } from './types/Logger.js'; /** @@ -114,6 +115,10 @@ import type { * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/ */ class Logger extends Utility implements LoggerInterface { + /** + * Replacer function used to format the log items. + */ + protected jsonReplacerFn?: CustomReplacerFn; /** * Console instance used to print logs. * @@ -920,7 +925,7 @@ class Logger extends Utility implements LoggerInterface { this.console[consoleMethod]( JSON.stringify( log.getAttributes(), - this.getReplacer(), + this.jsonReplacerFn, this.logIndentation ) ); @@ -1119,6 +1124,7 @@ class Logger extends Utility implements LoggerInterface { persistentKeys, persistentLogAttributes, // deprecated in favor of persistentKeys environment, + jsonReplacerFn, } = options; if (persistentLogAttributes && persistentKeys) { @@ -1143,6 +1149,7 @@ class Logger extends Utility implements LoggerInterface { this.setLogFormatter(logFormatter); this.setConsole(); this.setLogIndentation(); + this.#setJsonReplacerFn(jsonReplacerFn); return this; } @@ -1175,6 +1182,16 @@ class Logger extends Utility implements LoggerInterface { }); this.appendPersistentKeys(persistentLogAttributes); } + + /** + * It sets the JSON replacer function used to serialize the log items. + * @private + * @param customerReplacerFn + */ + #setJsonReplacerFn(customerReplacerFn?: CustomReplacerFn): void { + this.jsonReplacerFn = + customerReplacerFn ?? (this.getReplacer() as CustomReplacerFn); + } } export { Logger }; diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index 00af643eab..70541c7bbd 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -28,9 +28,6 @@ type InjectLambdaContextOptions = { resetKeys?: boolean; }; -/** - * Custom replacer function for JSON.stringify. - */ type CustomReplacerFn = (key: string, value: unknown) => void; type BaseConstructorOptions = { @@ -145,5 +142,6 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, + JsonReplacerFn, CustomReplacerFn, }; From 5122113e6e3f61de8ad59e910c2dd106f6de9c53 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 15:00:14 +0600 Subject: [PATCH 03/13] feat: pass `jsonReplacerFn` down to to child --- packages/logger/src/Logger.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index fca085a331..a97ac3bf01 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -314,6 +314,7 @@ class Logger extends Utility implements LoggerInterface { environment: this.powertoolsLogData.environment, persistentLogAttributes: this.persistentLogAttributes, temporaryLogAttributes: this.temporaryLogAttributes, + jsonReplacerFn: this.jsonReplacerFn, }, options ) From d139ca4be7ec0a711a7cbef777d7f962f28b96b7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 15:01:19 +0600 Subject: [PATCH 04/13] test: fix failing tests for `jsonReplacerFn` --- packages/logger/tests/unit/Logger.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index dd275a9be3..aac38f2d81 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -107,6 +107,7 @@ describe('Class: Logger', () => { defaultServiceName: 'service_undefined', customConfigService: expect.any(EnvironmentVariablesService), envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -149,6 +150,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -300,6 +302,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2527,6 +2530,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2555,6 +2559,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2583,6 +2588,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2659,6 +2665,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2687,6 +2694,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2718,6 +2726,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2746,6 +2755,7 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), + jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), From da5d748e9c54bfe229119812686ed05257e67a4e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 15:17:16 +0600 Subject: [PATCH 05/13] test: set custom replacer function if provided in both parent/child --- packages/logger/tests/unit/Logger.test.ts | 56 +++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index aac38f2d81..83ab568490 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -10,9 +10,10 @@ import { ConfigServiceInterface } from '../../src/types/ConfigServiceInterface.j import { EnvironmentVariablesService } from '../../src/config/EnvironmentVariablesService.js'; import { PowertoolsLogFormatter } from '../../src/formatter/PowertoolsLogFormatter.js'; import { LogLevelThresholds, LogLevel } from '../../src/types/Log.js'; -import type { - LogFunction, - ConstructorOptions, +import { + type LogFunction, + type ConstructorOptions, + type CustomReplacerFn, } from '../../src/types/Logger.js'; import { LogJsonIndent } from '../../src/constants.js'; import type { Context } from 'aws-lambda'; @@ -563,6 +564,37 @@ describe('Class: Logger', () => { }) ); }); + + test('when a custom json replacer function is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const mockReplacerFunction: CustomReplacerFn = jest.fn(); + const loggerOptions: ConstructorOptions = { + jsonReplacerFn: mockReplacerFunction, + }; + + // Act + const logger = new Logger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + persistentLogAttributes: {}, + powertoolsLogData: { + sampleRateValue: 0, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + defaultServiceName: 'service_undefined', + logLevel: 8, + logFormatter: expect.any(PowertoolsLogFormatter), + jsonReplacerFn: mockReplacerFunction, + }) + ); + }); }); describe.each([ @@ -2933,6 +2965,24 @@ describe('Class: Logger', () => { }) ); }); + + test('child logger should have the same jsonReplacerFn as its parent', () => { + // Prepare + const mockReplacerFunction: CustomReplacerFn = jest.fn(); + const parentLogger = new Logger({ + jsonReplacerFn: mockReplacerFunction, + }); + + // Act + const childLoggerWithParentLogFormatter = parentLogger.createChild(); + + // Assess + expect(childLoggerWithParentLogFormatter).toEqual( + expect.objectContaining({ + jsonReplacerFn: mockReplacerFunction, + }) + ); + }); }); describe('Method: logEventIfEnabled', () => { From f29a67ce6a26ff02e033c087a88b04ed01d3ce18 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 15:29:05 +0600 Subject: [PATCH 06/13] test: custom replacer function feature --- packages/logger/tests/unit/Logger.test.ts | 138 ++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index 83ab568490..aadb931ca5 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -1348,6 +1348,144 @@ describe('Class: Logger', () => { ); }); }); + + describe('Feature: custom replacer function', () => { + test('it should correctly serialize Set values using the provided jsonReplacerFn', () => { + const jsonReplacerFn: CustomReplacerFn = ( + key: string, + value: unknown + ) => (value instanceof Set ? [...value] : value); + + const logger = new Logger({ jsonReplacerFn }); + const consoleSpy = jest.spyOn( + logger['console'], + getConsoleMethod(methodOfLogger) + ); + const message = `This is an ${methodOfLogger} log with Set value`; + + const logItem = { value: new Set([1, 2]) }; + + // Act + logger[methodOfLogger](message, logItem); + + // Assess + expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenNthCalledWith( + 1, + JSON.stringify({ + level: methodOfLogger.toUpperCase(), + message: message, + sampling_rate: 0, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + value: [1, 2], + }) + ); + }); + + test('it is unable to serialize Set values while using the default jsonReplacerFn', () => { + const logger = new Logger(); + const consoleSpy = jest.spyOn( + logger['console'], + getConsoleMethod(methodOfLogger) + ); + const message = `This is an ${methodOfLogger} log with Set value`; + + const logItem = { value: new Set([1, 2]) }; + + // Act + logger[methodOfLogger](message, logItem); + + // Assess + expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenNthCalledWith( + 1, + JSON.stringify({ + level: methodOfLogger.toUpperCase(), + message: message, + sampling_rate: 0, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + value: {}, + }) + ); + }); + + test('it should correctly serialize Map values using the provided jsonReplacerFn', () => { + const jsonReplacerFn: CustomReplacerFn = ( + key: string, + value: unknown + ) => (value instanceof Map ? [...value] : value); + + const logger = new Logger({ jsonReplacerFn }); + const consoleSpy = jest.spyOn( + logger['console'], + getConsoleMethod(methodOfLogger) + ); + const message = `This is an ${methodOfLogger} log with Map value`; + + const mappedValue = new Map(); + mappedValue.set('foo', 'bar'); + mappedValue.set('baz', 'qux'); + + const logItem = { value: mappedValue }; + + // Act + logger[methodOfLogger](message, logItem); + + // Assess + expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenNthCalledWith( + 1, + JSON.stringify({ + level: methodOfLogger.toUpperCase(), + message: message, + sampling_rate: 0, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + value: [ + ['foo', 'bar'], + ['baz', 'qux'], + ], + }) + ); + }); + + test('it is unable to serialize Map values while using the default jsonReplacerFn', () => { + const logger = new Logger(); + const consoleSpy = jest.spyOn( + logger['console'], + getConsoleMethod(methodOfLogger) + ); + const message = `This is an ${methodOfLogger} log with Map value`; + const mappedValue = new Map(); + mappedValue.set('foo', 'bar'); + mappedValue.set('baz', 'qux'); + + const logItem = { value: mappedValue }; + + // Act + logger[methodOfLogger](message, logItem); + + // Assess + expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenNthCalledWith( + 1, + JSON.stringify({ + level: methodOfLogger.toUpperCase(), + message: message, + sampling_rate: 0, + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + value: {}, + }) + ); + }); + }); } ); From 3e137597dd20ee51d9b7ca61b8ba535e23f90c72 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 15:58:34 +0600 Subject: [PATCH 07/13] fix: export types --- packages/logger/src/types/Logger.ts | 1 - packages/logger/src/types/index.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index 70541c7bbd..1c900923cd 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -142,6 +142,5 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, - JsonReplacerFn, CustomReplacerFn, }; diff --git a/packages/logger/src/types/index.ts b/packages/logger/src/types/index.ts index e18a8806e0..afd097db67 100644 --- a/packages/logger/src/types/index.ts +++ b/packages/logger/src/types/index.ts @@ -14,4 +14,5 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, + CustomReplacerFn, } from './Logger.js'; From 4090ea15878bb040642c62e4f527497d7fc8ec84 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 16:00:23 +0600 Subject: [PATCH 08/13] refactor: rename `getReplacer` to `getDefaultReplacer` function --- packages/logger/src/Logger.ts | 70 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index a97ac3bf01..f785796eb0 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -789,6 +789,40 @@ class Logger extends Utility implements LoggerInterface { return this.customConfigService; } + /** + * When the data added in the log item contains object references or BigInt values, + * `JSON.stringify()` can't handle them and instead throws errors: + * `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`. + * To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions + * @private + */ + private getDefaultReplacer(): ( + key: string, + value: LogAttributes | Error | bigint + ) => void { + const references = new WeakSet(); + + return (key, value) => { + let item = value; + if (item instanceof Error) { + item = this.getLogFormatter().formatError(item); + } + if (typeof item === 'bigint') { + return item.toString(); + } + if (typeof item === 'object' && value !== null) { + if (references.has(item)) { + return; + } + references.add(item); + } + + return item; + }; + } + /** * It returns the instance of a service that fetches environment variables. * @@ -841,40 +875,6 @@ class Logger extends Utility implements LoggerInterface { return this.powertoolsLogData; } - /** - * When the data added in the log item contains object references or BigInt values, - * `JSON.stringify()` can't handle them and instead throws errors: - * `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`. - * To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings. - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions - * @private - */ - private getReplacer(): ( - key: string, - value: LogAttributes | Error | bigint - ) => void { - const references = new WeakSet(); - - return (key, value) => { - let item = value; - if (item instanceof Error) { - item = this.getLogFormatter().formatError(item); - } - if (typeof item === 'bigint') { - return item.toString(); - } - if (typeof item === 'object' && value !== null) { - if (references.has(item)) { - return; - } - references.add(item); - } - - return item; - }; - } - /** * It returns true and type guards the log level if a given log level is valid. * @@ -1191,7 +1191,7 @@ class Logger extends Utility implements LoggerInterface { */ #setJsonReplacerFn(customerReplacerFn?: CustomReplacerFn): void { this.jsonReplacerFn = - customerReplacerFn ?? (this.getReplacer() as CustomReplacerFn); + customerReplacerFn ?? (this.getDefaultReplacer() as CustomReplacerFn); } } From a5615bb64fbc22ede5da33ca53d24e729ab6d3a1 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 16:15:34 +0600 Subject: [PATCH 09/13] doc: Custom function for unserializable values --- docs/core/logger.md | 18 ++++++++++++++++++ .../snippets/logger/unserializableValues.json | 9 +++++++++ .../snippets/logger/unserializableValues.ts | 13 +++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 examples/snippets/logger/unserializableValues.json create mode 100644 examples/snippets/logger/unserializableValues.ts diff --git a/docs/core/logger.md b/docs/core/logger.md index dae36c9a37..8a494f3fe5 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -539,6 +539,24 @@ If you prefer to log in a specific timezone, you can configure it by setting the --8<-- "examples/snippets/logger/customTimezoneOutput.json" ``` +### Custom function for unserializable values + +By default, Logger uses `JSON.stringify()` to serialize log items. This means that `Map`, `Set` etc. will be serialized as `{}`, as detailed in the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description). + +You can manage the serialization of these types by providing your own replacer function, which will be utilized during serialization. + +=== "unserializableValues.ts" + + ```typescript hl_lines="4 7" + --8<-- "examples/snippets/logger/unserializableValues.ts" + ``` + +=== "unserializableValues.json" + + ```json hl_lines="8" + --8<-- "examples/snippets/logger/unserializableValues.json" + ``` + ### Using multiple Logger instances across your code The `createChild` method allows you to create a child instance of the Logger, which inherits all of the attributes from its parent. You have the option to override any of the settings and attributes from the parent logger, including [its settings](#utility-settings), any [extra keys](#appending-additional-keys), and [the log formatter](#custom-log-formatter-bring-your-own-formatter). diff --git a/examples/snippets/logger/unserializableValues.json b/examples/snippets/logger/unserializableValues.json new file mode 100644 index 0000000000..9134cdcbe3 --- /dev/null +++ b/examples/snippets/logger/unserializableValues.json @@ -0,0 +1,9 @@ +{ + "level": "INFO", + "message": "Serialize with custom serializer", + "sampling_rate": 0, + "service": "serverlessAirline", + "timestamp": "2024-07-07T09:52:14.212Z", + "xray_trace_id": "1-668a654d-396c646b760ee7d067f32f18", + "serializedValue": [1, 2, 3] +} diff --git a/examples/snippets/logger/unserializableValues.ts b/examples/snippets/logger/unserializableValues.ts new file mode 100644 index 0000000000..5ccf794787 --- /dev/null +++ b/examples/snippets/logger/unserializableValues.ts @@ -0,0 +1,13 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import type { CustomReplacerFn } from '@aws-lambda-powertools/logger/types'; + +const jsonReplacerFn: CustomReplacerFn = (key: string, value: unknown) => + value instanceof Set ? [...value] : value; + +const logger = new Logger({ serviceName: 'serverlessAirline', jsonReplacerFn }); + +export const handler = async (_event, _context): Promise => { + logger.info('Serialize with custom serializer', { + serializedValue: new Set([1, 2, 3]), + }); +}; From 0cff6371449fc8ac246d65b4c7b617e20b3ee48a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 7 Jul 2024 16:20:47 +0600 Subject: [PATCH 10/13] chore: minor doc update --- packages/logger/src/Logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index f785796eb0..93975a8866 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -116,7 +116,7 @@ import type { */ class Logger extends Utility implements LoggerInterface { /** - * Replacer function used to format the log items. + * Replacer function used to serialize the log items. */ protected jsonReplacerFn?: CustomReplacerFn; /** @@ -1185,7 +1185,7 @@ class Logger extends Utility implements LoggerInterface { } /** - * It sets the JSON replacer function used to serialize the log items. + * It sets the JSON replacer function which is used to serialize the log items. * @private * @param customerReplacerFn */ From 2f66bbae6f8a6d706308b8c30fb06f8aa8b0f551 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 11 Jul 2024 13:14:41 +0200 Subject: [PATCH 11/13] chore: address PR review --- docs/core/logger.md | 2 +- .../snippets/logger/unserializableValues.ts | 4 +- packages/logger/src/Logger.ts | 92 ++++------ packages/logger/src/types/Logger.ts | 15 +- packages/logger/src/types/index.ts | 2 +- packages/logger/tests/unit/Logger.test.ts | 171 ++++-------------- 6 files changed, 90 insertions(+), 196 deletions(-) diff --git a/docs/core/logger.md b/docs/core/logger.md index 8a494f3fe5..03f7ba5cc3 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -547,7 +547,7 @@ You can manage the serialization of these types by providing your own replacer f === "unserializableValues.ts" - ```typescript hl_lines="4 7" + ```typescript hl_lines="4-5 7" --8<-- "examples/snippets/logger/unserializableValues.ts" ``` diff --git a/examples/snippets/logger/unserializableValues.ts b/examples/snippets/logger/unserializableValues.ts index 5ccf794787..0a5ff0e2c7 100644 --- a/examples/snippets/logger/unserializableValues.ts +++ b/examples/snippets/logger/unserializableValues.ts @@ -1,12 +1,12 @@ import { Logger } from '@aws-lambda-powertools/logger'; import type { CustomReplacerFn } from '@aws-lambda-powertools/logger/types'; -const jsonReplacerFn: CustomReplacerFn = (key: string, value: unknown) => +const jsonReplacerFn: CustomReplacerFn = (_: string, value: unknown) => value instanceof Set ? [...value] : value; const logger = new Logger({ serviceName: 'serverlessAirline', jsonReplacerFn }); -export const handler = async (_event, _context): Promise => { +export const handler = async (): Promise => { logger.info('Serialize with custom serializer', { serializedValue: new Set([1, 2, 3]), }); diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 93975a8866..b56ec6a707 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -24,7 +24,7 @@ import type { LogItemMessage, LoggerInterface, PowertoolsLogData, - CustomReplacerFn, + CustomJsonReplacerFn, } from './types/Logger.js'; /** @@ -115,10 +115,6 @@ import type { * @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/ */ class Logger extends Utility implements LoggerInterface { - /** - * Replacer function used to serialize the log items. - */ - protected jsonReplacerFn?: CustomReplacerFn; /** * Console instance used to print logs. * @@ -205,6 +201,10 @@ class Logger extends Utility implements LoggerInterface { * We keep this value to be able to reset the log level to the initial value when the sample rate is refreshed. */ #initialLogLevel = 12; + /** + * Replacer function used to serialize the log items. + */ + #jsonReplacerFn?: CustomJsonReplacerFn; /** * Log level used by the current instance of Logger. @@ -314,7 +314,7 @@ class Logger extends Utility implements LoggerInterface { environment: this.powertoolsLogData.environment, persistentLogAttributes: this.persistentLogAttributes, temporaryLogAttributes: this.temporaryLogAttributes, - jsonReplacerFn: this.jsonReplacerFn, + jsonReplacerFn: this.#jsonReplacerFn, }, options ) @@ -680,6 +680,38 @@ class Logger extends Utility implements LoggerInterface { return new Logger(options); } + /** + * When the data added in the log item contains object references or BigInt values, + * `JSON.stringify()` can't handle them and instead throws errors: + * `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`. + * To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions + * @private + */ + protected getJsonReplacer(): (key: string, value: unknown) => void { + const references = new WeakSet(); + + return (key, value) => { + if (this.#jsonReplacerFn) value = this.#jsonReplacerFn?.(key, value); + + if (value instanceof Error) { + value = this.getLogFormatter().formatError(value); + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (typeof value === 'object' && value !== null) { + if (references.has(value)) { + return; + } + references.add(value); + } + + return value; + }; + } + /** * It stores information that is printed in all log items. * @@ -789,40 +821,6 @@ class Logger extends Utility implements LoggerInterface { return this.customConfigService; } - /** - * When the data added in the log item contains object references or BigInt values, - * `JSON.stringify()` can't handle them and instead throws errors: - * `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`. - * To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings. - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions - * @private - */ - private getDefaultReplacer(): ( - key: string, - value: LogAttributes | Error | bigint - ) => void { - const references = new WeakSet(); - - return (key, value) => { - let item = value; - if (item instanceof Error) { - item = this.getLogFormatter().formatError(item); - } - if (typeof item === 'bigint') { - return item.toString(); - } - if (typeof item === 'object' && value !== null) { - if (references.has(item)) { - return; - } - references.add(item); - } - - return item; - }; - } - /** * It returns the instance of a service that fetches environment variables. * @@ -926,7 +924,7 @@ class Logger extends Utility implements LoggerInterface { this.console[consoleMethod]( JSON.stringify( log.getAttributes(), - this.jsonReplacerFn, + this.getJsonReplacer(), this.logIndentation ) ); @@ -1150,7 +1148,7 @@ class Logger extends Utility implements LoggerInterface { this.setLogFormatter(logFormatter); this.setConsole(); this.setLogIndentation(); - this.#setJsonReplacerFn(jsonReplacerFn); + this.#jsonReplacerFn = jsonReplacerFn; return this; } @@ -1183,16 +1181,6 @@ class Logger extends Utility implements LoggerInterface { }); this.appendPersistentKeys(persistentLogAttributes); } - - /** - * It sets the JSON replacer function which is used to serialize the log items. - * @private - * @param customerReplacerFn - */ - #setJsonReplacerFn(customerReplacerFn?: CustomReplacerFn): void { - this.jsonReplacerFn = - customerReplacerFn ?? (this.getDefaultReplacer() as CustomReplacerFn); - } } export { Logger }; diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index 1c900923cd..6f65d2f814 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -28,7 +28,13 @@ type InjectLambdaContextOptions = { resetKeys?: boolean; }; -type CustomReplacerFn = (key: string, value: unknown) => void; +/** + * A custom JSON replacer function that can be passed to the Logger constructor. + * + * @param key - The key of the value being stringified. + * @param value - The value being stringified. + */ +type CustomJsonReplacerFn = (key: string, value: unknown) => unknown; type BaseConstructorOptions = { logLevel?: LogLevel; @@ -37,7 +43,10 @@ type BaseConstructorOptions = { logFormatter?: LogFormatterInterface; customConfigService?: ConfigServiceInterface; environment?: Environment; - jsonReplacerFn?: CustomReplacerFn; + /** + * A custom JSON replacer function that can be passed to the Logger constructor. + */ + jsonReplacerFn?: CustomJsonReplacerFn; }; type PersistentKeysOption = { @@ -142,5 +151,5 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, - CustomReplacerFn, + CustomJsonReplacerFn, }; diff --git a/packages/logger/src/types/index.ts b/packages/logger/src/types/index.ts index afd097db67..6d472920b8 100644 --- a/packages/logger/src/types/index.ts +++ b/packages/logger/src/types/index.ts @@ -14,5 +14,5 @@ export type { PowertoolsLogData, ConstructorOptions, InjectLambdaContextOptions, - CustomReplacerFn, + CustomJsonReplacerFn, } from './Logger.js'; diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index aadb931ca5..1cb720e9db 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -13,7 +13,7 @@ import { LogLevelThresholds, LogLevel } from '../../src/types/Log.js'; import { type LogFunction, type ConstructorOptions, - type CustomReplacerFn, + type CustomJsonReplacerFn, } from '../../src/types/Logger.js'; import { LogJsonIndent } from '../../src/constants.js'; import type { Context } from 'aws-lambda'; @@ -108,7 +108,6 @@ describe('Class: Logger', () => { defaultServiceName: 'service_undefined', customConfigService: expect.any(EnvironmentVariablesService), envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -151,7 +150,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -303,7 +301,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: 0, logFormatter: expect.any(PowertoolsLogFormatter), @@ -564,37 +561,6 @@ describe('Class: Logger', () => { }) ); }); - - test('when a custom json replacer function is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const mockReplacerFunction: CustomReplacerFn = jest.fn(); - const loggerOptions: ConstructorOptions = { - jsonReplacerFn: mockReplacerFunction, - }; - - // Act - const logger = new Logger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - persistentLogAttributes: {}, - powertoolsLogData: { - sampleRateValue: 0, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - defaultServiceName: 'service_undefined', - logLevel: 8, - logFormatter: expect.any(PowertoolsLogFormatter), - jsonReplacerFn: mockReplacerFunction, - }) - ); - }); }); describe.each([ @@ -1225,7 +1191,7 @@ describe('Class: Logger', () => { }); }); - describe('Feature: handle safely unexpected errors', () => { + describe('Feature: custom JSON replacer function', () => { test('when a logged item references itself, the logger ignores the keys that cause a circular reference', () => { // Prepare const logger = new Logger({ @@ -1347,12 +1313,11 @@ describe('Class: Logger', () => { }) ); }); - }); - describe('Feature: custom replacer function', () => { - test('it should correctly serialize Set values using the provided jsonReplacerFn', () => { - const jsonReplacerFn: CustomReplacerFn = ( - key: string, + it('should correctly serialize custom values using the provided jsonReplacerFn', () => { + // Prepare + const jsonReplacerFn: CustomJsonReplacerFn = ( + _: string, value: unknown ) => (value instanceof Set ? [...value] : value); @@ -1369,7 +1334,7 @@ describe('Class: Logger', () => { logger[methodOfLogger](message, logItem); // Assess - expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledTimes(1); expect(consoleSpy).toHaveBeenNthCalledWith( 1, JSON.stringify({ @@ -1384,59 +1349,33 @@ describe('Class: Logger', () => { ); }); - test('it is unable to serialize Set values while using the default jsonReplacerFn', () => { - const logger = new Logger(); - const consoleSpy = jest.spyOn( - logger['console'], - getConsoleMethod(methodOfLogger) - ); - const message = `This is an ${methodOfLogger} log with Set value`; - - const logItem = { value: new Set([1, 2]) }; - - // Act - logger[methodOfLogger](message, logItem); - - // Assess - expect(consoleSpy).toBeCalledTimes(1); - expect(consoleSpy).toHaveBeenNthCalledWith( - 1, - JSON.stringify({ - level: methodOfLogger.toUpperCase(), - message: message, - sampling_rate: 0, - service: 'hello-world', - timestamp: '2016-06-20T12:08:10.000Z', - xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', - value: {}, - }) - ); - }); - - test('it should correctly serialize Map values using the provided jsonReplacerFn', () => { - const jsonReplacerFn: CustomReplacerFn = ( - key: string, + it('should serialize using both the existing replacer and the customer-provided one', () => { + // Prepare + const jsonReplacerFn: CustomJsonReplacerFn = ( + _: string, value: unknown - ) => (value instanceof Map ? [...value] : value); + ) => { + if (value instanceof Set || value instanceof Map) { + return [...value]; + } + + return value; + }; const logger = new Logger({ jsonReplacerFn }); const consoleSpy = jest.spyOn( logger['console'], getConsoleMethod(methodOfLogger) ); - const message = `This is an ${methodOfLogger} log with Map value`; - const mappedValue = new Map(); - mappedValue.set('foo', 'bar'); - mappedValue.set('baz', 'qux'); - - const logItem = { value: mappedValue }; + const message = `This is an ${methodOfLogger} log with Set value`; + const logItem = { value: new Set([1, 2]), number: BigInt(42) }; // Act logger[methodOfLogger](message, logItem); // Assess - expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledTimes(1); expect(consoleSpy).toHaveBeenNthCalledWith( 1, JSON.stringify({ @@ -1446,44 +1385,27 @@ describe('Class: Logger', () => { service: 'hello-world', timestamp: '2016-06-20T12:08:10.000Z', xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', - value: [ - ['foo', 'bar'], - ['baz', 'qux'], - ], + value: [1, 2], + number: '42', }) ); }); - test('it is unable to serialize Map values while using the default jsonReplacerFn', () => { - const logger = new Logger(); - const consoleSpy = jest.spyOn( - logger['console'], - getConsoleMethod(methodOfLogger) - ); - const message = `This is an ${methodOfLogger} log with Map value`; - const mappedValue = new Map(); - mappedValue.set('foo', 'bar'); - mappedValue.set('baz', 'qux'); - - const logItem = { value: mappedValue }; + it('should pass the JSON customer-provided replacer function to child loggers', () => { + // Prepare + const jsonReplacerFn: CustomJsonReplacerFn = ( + _: string, + value: unknown + ) => (value instanceof Set ? [...value] : value); + const logger = new Logger({ jsonReplacerFn }); // Act - logger[methodOfLogger](message, logItem); + const childLogger = logger.createChild(); // Assess - expect(consoleSpy).toBeCalledTimes(1); - expect(consoleSpy).toHaveBeenNthCalledWith( - 1, - JSON.stringify({ - level: methodOfLogger.toUpperCase(), - message: message, - sampling_rate: 0, - service: 'hello-world', - timestamp: '2016-06-20T12:08:10.000Z', - xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', - value: {}, - }) - ); + expect(() => + childLogger.info('foo', { foo: new Set([1, 2]) }) + ).not.toThrow(); }); }); } @@ -2700,7 +2622,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2729,7 +2650,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2758,7 +2678,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2835,7 +2754,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2864,7 +2782,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2896,7 +2813,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -2925,7 +2841,6 @@ describe('Class: Logger', () => { customConfigService: undefined, defaultServiceName: 'service_undefined', envVarsService: expect.any(EnvironmentVariablesService), - jsonReplacerFn: expect.any(Function), logEvent: false, logIndentation: INDENTATION, logFormatter: expect.any(PowertoolsLogFormatter), @@ -3103,24 +3018,6 @@ describe('Class: Logger', () => { }) ); }); - - test('child logger should have the same jsonReplacerFn as its parent', () => { - // Prepare - const mockReplacerFunction: CustomReplacerFn = jest.fn(); - const parentLogger = new Logger({ - jsonReplacerFn: mockReplacerFunction, - }); - - // Act - const childLoggerWithParentLogFormatter = parentLogger.createChild(); - - // Assess - expect(childLoggerWithParentLogFormatter).toEqual( - expect.objectContaining({ - jsonReplacerFn: mockReplacerFunction, - }) - ); - }); }); describe('Method: logEventIfEnabled', () => { From c2e2285026bceb8f7e98192851a300d1d97137c5 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 11 Jul 2024 13:27:44 +0200 Subject: [PATCH 12/13] docs: improve docs --- docs/core/logger.md | 38 +++++++++++++++-------------- packages/logger/src/Logger.ts | 16 +++++++----- packages/logger/src/types/Logger.ts | 14 +++++++++-- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/docs/core/logger.md b/docs/core/logger.md index 03f7ba5cc3..b858989e60 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -539,24 +539,6 @@ If you prefer to log in a specific timezone, you can configure it by setting the --8<-- "examples/snippets/logger/customTimezoneOutput.json" ``` -### Custom function for unserializable values - -By default, Logger uses `JSON.stringify()` to serialize log items. This means that `Map`, `Set` etc. will be serialized as `{}`, as detailed in the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description). - -You can manage the serialization of these types by providing your own replacer function, which will be utilized during serialization. - -=== "unserializableValues.ts" - - ```typescript hl_lines="4-5 7" - --8<-- "examples/snippets/logger/unserializableValues.ts" - ``` - -=== "unserializableValues.json" - - ```json hl_lines="8" - --8<-- "examples/snippets/logger/unserializableValues.json" - ``` - ### Using multiple Logger instances across your code The `createChild` method allows you to create a child instance of the Logger, which inherits all of the attributes from its parent. You have the option to override any of the settings and attributes from the parent logger, including [its settings](#utility-settings), any [extra keys](#appending-additional-keys), and [the log formatter](#custom-log-formatter-bring-your-own-formatter). @@ -778,6 +760,26 @@ This is how the printed log would look: !!! tip "Custom Log formatter and Child loggers" It is not necessary to pass the `LogFormatter` each time a [child logger](#using-multiple-logger-instances-across-your-code) is created. The parent's LogFormatter will be inherited by the child logger. +### Bring your own JSON serializer + +You can extend the default JSON serializer by passing a custom serializer function to the `Logger` constructor, using the `jsonReplacerFn` option. This is useful when you need to serialize custom objects or when you want to customize the serialization of specific values. + +=== "unserializableValues.ts" + + ```typescript hl_lines="4-5 7" + --8<-- "examples/snippets/logger/unserializableValues.ts" + ``` + +=== "unserializableValues.json" + + ```json hl_lines="8" + --8<-- "examples/snippets/logger/unserializableValues.json" + ``` + +By default, Logger uses `JSON.stringify()` to serialize log items and a [custom replacer function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter) to serialize common unserializable values such as `BigInt`, circular references, and `Error` objects. + +When you extend the default JSON serializer, your custom serializer function will be called before the default one. This allows you to customize the serialization while still benefiting from the default behavior. + ## Testing your code ### Inject Lambda Context diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index b56ec6a707..d97e6c2177 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -681,13 +681,17 @@ class Logger extends Utility implements LoggerInterface { } /** - * When the data added in the log item contains object references or BigInt values, - * `JSON.stringify()` can't handle them and instead throws errors: - * `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`. - * To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings. + * A custom JSON replacer function that is used to serialize the log items. * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions - * @private + * By default, we already extend the default serialization behavior to handle `BigInt` and `Error` objects, as well as remove circular references. + * When a custom JSON replacer function is passed to the Logger constructor, it will be called **before** our custom rules for each key-value pair in the object being stringified. + * + * This allows you to customize the serialization while still benefiting from the default behavior. + * + * @see {@link ConstructorOptions.jsonReplacerFn} + * + * @param key - The key of the value being stringified. + * @param value - The value being stringified. */ protected getJsonReplacer(): (key: string, value: unknown) => void { const references = new WeakSet(); diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index 6f65d2f814..51585188d0 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -29,7 +29,12 @@ type InjectLambdaContextOptions = { }; /** - * A custom JSON replacer function that can be passed to the Logger constructor. + * A custom JSON replacer function that can be passed to the Logger constructor to extend the default serialization behavior. + * + * By default, we already extend the default serialization behavior to handle `BigInt` and `Error` objects, as well as remove circular references. + * When a custom JSON replacer function is passed to the Logger constructor, it will be called **before** our custom rules for each key-value pair in the object being stringified. + * + * This allows you to customize the serialization while still benefiting from the default behavior. * * @param key - The key of the value being stringified. * @param value - The value being stringified. @@ -44,7 +49,12 @@ type BaseConstructorOptions = { customConfigService?: ConfigServiceInterface; environment?: Environment; /** - * A custom JSON replacer function that can be passed to the Logger constructor. + * A custom JSON replacer function that can be passed to the Logger constructor to extend the default serialization behavior. + * + * By default, we already extend the default serialization behavior to handle `BigInt` and `Error` objects, as well as remove circular references. + * When a custom JSON replacer function is passed to the Logger constructor, it will be called **before** our custom rules for each key-value pair in the object being stringified. + * + * This allows you to customize the serialization while still benefiting from the default behavior. */ jsonReplacerFn?: CustomJsonReplacerFn; }; From 147ed40a310fe42d637625707a134c59d5fe16ec Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 11 Jul 2024 15:49:06 +0200 Subject: [PATCH 13/13] docs: address feedback --- docs/core/logger.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/core/logger.md b/docs/core/logger.md index b858989e60..47f9f00d85 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -762,7 +762,7 @@ This is how the printed log would look: ### Bring your own JSON serializer -You can extend the default JSON serializer by passing a custom serializer function to the `Logger` constructor, using the `jsonReplacerFn` option. This is useful when you need to serialize custom objects or when you want to customize the serialization of specific values. +You can extend the default JSON serializer by passing a custom serializer function to the `Logger` constructor, using the `jsonReplacerFn` option. This is useful when you want to customize the serialization of specific values. === "unserializableValues.ts" @@ -778,7 +778,7 @@ You can extend the default JSON serializer by passing a custom serializer functi By default, Logger uses `JSON.stringify()` to serialize log items and a [custom replacer function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter) to serialize common unserializable values such as `BigInt`, circular references, and `Error` objects. -When you extend the default JSON serializer, your custom serializer function will be called before the default one. This allows you to customize the serialization while still benefiting from the default behavior. +When you extend the default JSON serializer, we will call your custom serializer function before the default one. This allows you to customize the serialization while still benefiting from the default behavior. ## Testing your code