Skip to content

Commit a09e4df

Browse files
authored
fix(logger): logger throws TypeError when log item has BigInt value (#1201)
* fix(logger): fix typeError when a log item has BigInt value, add tests * refactor(logger): add a test and rewrite removeEmptyKeys without pickby dependency * remove dependencies
1 parent b4cb3d9 commit a09e4df

File tree

5 files changed

+131
-94
lines changed

5 files changed

+131
-94
lines changed

Diff for: package-lock.json

+4-36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: packages/logger/package.json

+3-5
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
"types": "./lib/index.d.ts",
3333
"typedocMain": "src/index.ts",
3434
"devDependencies": {
35-
"@types/lodash.merge": "^4.6.7",
36-
"@types/lodash.pickby": "^4.6.7"
35+
"@types/lodash.merge": "^4.6.7"
3736
},
3837
"files": [
3938
"lib"
@@ -47,8 +46,7 @@
4746
},
4847
"dependencies": {
4948
"@aws-lambda-powertools/commons": "^1.5.0",
50-
"lodash.merge": "^4.6.2",
51-
"lodash.pickby": "^4.6.0"
49+
"lodash.merge": "^4.6.2"
5250
},
5351
"keywords": [
5452
"aws",
@@ -59,4 +57,4 @@
5957
"serverless",
6058
"nodejs"
6159
]
62-
}
60+
}

Diff for: packages/logger/src/Logger.ts

+32-28
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,37 @@ class Logger extends Utility implements ClassThatLogs {
568568
return this.powertoolLogData;
569569
}
570570

571+
/**
572+
* When the data added in the log item contains object references or BigInt values,
573+
* `JSON.stringify()` can't handle them and instead throws errors:
574+
* `TypeError: cyclic object value` or `TypeError: Do not know how to serialize a BigInt`.
575+
* To mitigate these issues, this method will find and remove all cyclic references and convert BigInt values to strings.
576+
*
577+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions
578+
* @private
579+
*/
580+
private getReplacer(): (key: string, value: LogAttributes | Error | bigint) => void {
581+
const references = new WeakSet();
582+
583+
return (key, value) => {
584+
let item = value;
585+
if (item instanceof Error) {
586+
item = this.getLogFormatter().formatError(item);
587+
}
588+
if (typeof item === 'bigint') {
589+
return item.toString();
590+
}
591+
if (typeof item === 'object' && value !== null) {
592+
if (references.has(item)) {
593+
return;
594+
}
595+
references.add(item);
596+
}
597+
598+
return item;
599+
};
600+
}
601+
571602
/**
572603
* It returns the numeric sample rate value.
573604
*
@@ -605,7 +636,7 @@ class Logger extends Utility implements ClassThatLogs {
605636

606637
const consoleMethod = logLevel.toLowerCase() as keyof ClassThatLogs;
607638

608-
this.console[consoleMethod](JSON.stringify(log.getAttributes(), this.removeCircularDependencies(), this.logIndentation));
639+
this.console[consoleMethod](JSON.stringify(log.getAttributes(), this.getReplacer(), this.logIndentation));
609640
}
610641

611642
/**
@@ -622,33 +653,6 @@ class Logger extends Utility implements ClassThatLogs {
622653
this.printLog(logLevel, this.createAndPopulateLogItem(logLevel, input, extraInput));
623654
}
624655

625-
/**
626-
* When the data added in the log item contains object references,
627-
* JSON.stringify() doesn't try to solve them and instead throws an error: TypeError: cyclic object value.
628-
* To mitigate this issue, this method will find and remove all cyclic references.
629-
*
630-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
631-
* @private
632-
*/
633-
private removeCircularDependencies(): (key: string, value: LogAttributes | Error) => void {
634-
const references = new WeakSet();
635-
636-
return (key, value) => {
637-
let item = value;
638-
if (item instanceof Error) {
639-
item = this.getLogFormatter().formatError(item);
640-
}
641-
if (typeof item === 'object' && value !== null) {
642-
if (references.has(item)) {
643-
return;
644-
}
645-
references.add(item);
646-
}
647-
648-
return item;
649-
};
650-
}
651-
652656
/**
653657
* It initializes console property as an instance of the internal version of Console() class (PR #748)
654658
* or as the global node console if the `POWERTOOLS_DEV' env variable is set and has truthy value.

Diff for: packages/logger/src/log/LogItem.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pickBy from 'lodash.pickby';
21
import merge from 'lodash.merge';
32
import { LogItemInterface } from '.';
43
import { LogAttributes } from '../types';
@@ -31,7 +30,14 @@ class LogItem implements LogItemInterface {
3130
}
3231

3332
public removeEmptyKeys(attributes: LogAttributes): LogAttributes {
34-
return pickBy(attributes, (value) => value !== undefined && value !== '' && value !== null);
33+
const newAttributes: LogAttributes = {};
34+
for (const key in attributes) {
35+
if (attributes[key] !== undefined && attributes[key] !== '' && attributes[key] !== null) {
36+
newAttributes[key] = attributes[key];
37+
}
38+
}
39+
40+
return newAttributes;
3541
}
3642

3743
public setAttributes(attributes: LogAttributes): void {

Diff for: packages/logger/tests/unit/Logger.test.ts

+84-23
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,68 @@ describe('Class: Logger', () => {
533533

534534
});
535535

536+
test('when a logged item has BigInt value, it doen\'t throw TypeError', () => {
537+
538+
// Prepare
539+
const logger = new Logger();
540+
jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
541+
const message = `This is an ${methodOfLogger} log with BigInt value`;
542+
const logItem = { value: BigInt(42) };
543+
const errorMessage = 'Do not know how to serialize a BigInt';
544+
545+
// Act & Assess
546+
expect(() => { logger[methodOfLogger](message, logItem); }).not.toThrow(errorMessage);
547+
548+
});
549+
550+
test('when a logged item has a BigInt value, it prints the log with value as a string', () => {
551+
552+
// Prepare
553+
const logger = new Logger();
554+
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
555+
const message = `This is an ${methodOfLogger} log with BigInt value`;
556+
const logItem = { value: BigInt(42) };
557+
558+
// Act
559+
logger[methodOfLogger](message, logItem);
560+
561+
// Assess
562+
expect(consoleSpy).toBeCalledTimes(1);
563+
expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({
564+
level: methodOfLogger.toUpperCase(),
565+
message: message,
566+
service: 'hello-world',
567+
timestamp: '2016-06-20T12:08:10.000Z',
568+
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
569+
value: '42',
570+
}));
571+
572+
});
573+
574+
test('when a logged item has empty string, null, or undefined values, it removes it', () => {
575+
576+
// Prepare
577+
const logger = new Logger();
578+
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
579+
const message = `This is an ${methodOfLogger} log with empty, null, and undefined values`;
580+
const logItem = { value: 42, emptyValue: '', undefinedValue: undefined, nullValue: null };
581+
582+
// Act
583+
logger[methodOfLogger](message, logItem);
584+
585+
// Assess
586+
expect(consoleSpy).toBeCalledTimes(1);
587+
expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({
588+
level: methodOfLogger.toUpperCase(),
589+
message: message,
590+
service: 'hello-world',
591+
timestamp: '2016-06-20T12:08:10.000Z',
592+
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
593+
value: 42,
594+
}));
595+
596+
});
597+
536598
});
537599
});
538600

@@ -765,34 +827,33 @@ describe('Class: Logger', () => {
765827
}));
766828
});
767829

768-
});
769-
770-
test('when called multiple times with the same keys, the outcome is the same', () => {
771-
772-
// Prepare
773-
const logger = new Logger();
774-
logger.appendKeys({
775-
aws_account_id: '123456789012',
776-
aws_region: 'eu-west-1',
777-
logger: {
778-
name: 'aws-lambda-powertool-typescript',
779-
version: '0.2.4',
780-
},
781-
});
782-
783-
// Act
784-
logger.removeKeys([ 'aws_account_id', 'aws_region' ]);
785-
logger.removeKeys([ 'aws_account_id', 'aws_region' ]);
830+
test('when called multiple times with the same keys, the outcome is the same', () => {
786831

787-
// Assess
788-
expect(logger).toEqual(expect.objectContaining({
789-
persistentLogAttributes: {
832+
// Prepare
833+
const logger = new Logger();
834+
logger.appendKeys({
835+
aws_account_id: '123456789012',
836+
aws_region: 'eu-west-1',
790837
logger: {
791838
name: 'aws-lambda-powertool-typescript',
792839
version: '0.2.4',
793840
},
794-
},
795-
}));
841+
});
842+
843+
// Act
844+
logger.removeKeys([ 'aws_account_id', 'aws_region' ]);
845+
logger.removeKeys([ 'aws_account_id', 'aws_region' ]);
846+
847+
// Assess
848+
expect(logger).toEqual(expect.objectContaining({
849+
persistentLogAttributes: {
850+
logger: {
851+
name: 'aws-lambda-powertool-typescript',
852+
version: '0.2.4',
853+
},
854+
},
855+
}));
856+
});
796857

797858
});
798859

0 commit comments

Comments
 (0)