Skip to content

feat(logger): custom function for unserializable values (JSON replacer) #2739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/core/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
9 changes: 9 additions & 0 deletions examples/snippets/logger/unserializableValues.json
Original file line number Diff line number Diff line change
@@ -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]
}
13 changes: 13 additions & 0 deletions examples/snippets/logger/unserializableValues.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
logger.info('Serialize with custom serializer', {
serializedValue: new Set([1, 2, 3]),
});
};
88 changes: 53 additions & 35 deletions packages/logger/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
LogItemMessage,
LoggerInterface,
PowertoolsLogData,
CustomReplacerFn,
} from './types/Logger.js';

/**
Expand Down Expand Up @@ -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 serialize the log items.
*/
protected jsonReplacerFn?: CustomReplacerFn;
/**
* Console instance used to print logs.
*
Expand Down Expand Up @@ -309,6 +314,7 @@ class Logger extends Utility implements LoggerInterface {
environment: this.powertoolsLogData.environment,
persistentLogAttributes: this.persistentLogAttributes,
temporaryLogAttributes: this.temporaryLogAttributes,
jsonReplacerFn: this.jsonReplacerFn,
},
options
)
Expand Down Expand Up @@ -783,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.
*
Expand Down Expand Up @@ -835,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.
*
Expand Down Expand Up @@ -920,7 +926,7 @@ class Logger extends Utility implements LoggerInterface {
this.console[consoleMethod](
JSON.stringify(
log.getAttributes(),
this.getReplacer(),
this.jsonReplacerFn,
this.logIndentation
)
);
Expand Down Expand Up @@ -1119,6 +1125,7 @@ class Logger extends Utility implements LoggerInterface {
persistentKeys,
persistentLogAttributes, // deprecated in favor of persistentKeys
environment,
jsonReplacerFn,
} = options;

if (persistentLogAttributes && persistentKeys) {
Expand All @@ -1143,6 +1150,7 @@ class Logger extends Utility implements LoggerInterface {
this.setLogFormatter(logFormatter);
this.setConsole();
this.setLogIndentation();
this.#setJsonReplacerFn(jsonReplacerFn);

return this;
}
Expand Down Expand Up @@ -1175,6 +1183,16 @@ 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 };
4 changes: 4 additions & 0 deletions packages/logger/src/types/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ type InjectLambdaContextOptions = {
resetKeys?: boolean;
};

type CustomReplacerFn = (key: string, value: unknown) => void;

type BaseConstructorOptions = {
logLevel?: LogLevel;
serviceName?: string;
sampleRateValue?: number;
logFormatter?: LogFormatterInterface;
customConfigService?: ConfigServiceInterface;
environment?: Environment;
jsonReplacerFn?: CustomReplacerFn;
};

type PersistentKeysOption = {
Expand Down Expand Up @@ -139,4 +142,5 @@ export type {
PowertoolsLogData,
ConstructorOptions,
InjectLambdaContextOptions,
CustomReplacerFn,
};
1 change: 1 addition & 0 deletions packages/logger/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type {
PowertoolsLogData,
ConstructorOptions,
InjectLambdaContextOptions,
CustomReplacerFn,
} from './Logger.js';
Loading