Skip to content

Commit 7293cc1

Browse files
committed
fix(logger): prevent overwriting standard keys
1 parent 11909ae commit 7293cc1

File tree

4 files changed

+179
-13
lines changed

4 files changed

+179
-13
lines changed

packages/logger/src/Logger.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
import type { Context, Handler } from 'aws-lambda';
1010
import merge from 'lodash.merge';
1111
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
12-
import { LogJsonIndent, LogLevelThreshold } from './constants.js';
12+
import { LogJsonIndent, LogLevelThreshold, ReservedKeys } from './constants.js';
1313
import type { LogFormatter } from './formatter/LogFormatter.js';
1414
import type { LogItem } from './formatter/LogItem.js';
1515
import { PowertoolsLogFormatter } from './formatter/PowertoolsLogFormatter.js';
@@ -221,10 +221,7 @@ class Logger extends Utility implements LoggerInterface {
221221
* @param attributes - The attributes to add to all log items.
222222
*/
223223
public appendKeys(attributes: LogAttributes): void {
224-
for (const attributeKey of Object.keys(attributes)) {
225-
this.#keys.set(attributeKey, 'temp');
226-
}
227-
merge(this.temporaryLogAttributes, attributes);
224+
this.#appendAnyKeys(attributes, 'temp');
228225
}
229226

230227
/**
@@ -233,10 +230,7 @@ class Logger extends Utility implements LoggerInterface {
233230
* @param attributes - The attributes to add to all log items.
234231
*/
235232
public appendPersistentKeys(attributes: LogAttributes): void {
236-
for (const attributeKey of Object.keys(attributes)) {
237-
this.#keys.set(attributeKey, 'persistent');
238-
}
239-
merge(this.persistentLogAttributes, attributes);
233+
this.#appendAnyKeys(attributes, 'persistent');
240234
}
241235

242236
/**
@@ -293,6 +287,13 @@ class Logger extends Utility implements LoggerInterface {
293287
* @param extraInput - The extra input to log.
294288
*/
295289
public debug(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
290+
for (const extra of extraInput) {
291+
if (!(extra instanceof Error) && !(typeof extra === 'string')) {
292+
for (const key of Object.keys(extra)) {
293+
this.#checkReservedKeyAndWarn(key);
294+
}
295+
}
296+
}
296297
this.processLogItem(LogLevelThreshold.DEBUG, input, extraInput);
297298
}
298299

@@ -666,6 +667,25 @@ class Logger extends Utility implements LoggerInterface {
666667
merge(this.powertoolsLogData, attributes);
667668
}
668669

670+
/**
671+
* Shared logic for adding keys to the logger instance.
672+
*
673+
* @param attributes - The attributes to add to the log item.
674+
* @param type - The type of the attributes to add.
675+
*/
676+
#appendAnyKeys(attributes: LogAttributes, type: 'temp' | 'persistent'): void {
677+
for (const attributeKey of Object.keys(attributes)) {
678+
if (this.#checkReservedKeyAndWarn(attributeKey) === false) {
679+
this.#keys.set(attributeKey, 'temp');
680+
}
681+
}
682+
if (type === 'temp') {
683+
merge(this.temporaryLogAttributes, attributes);
684+
} else {
685+
merge(this.persistentLogAttributes, attributes);
686+
}
687+
}
688+
669689
private awsLogLevelShortCircuit(selectedLogLevel?: string): boolean {
670690
const awsLogLevel = this.getEnvVarsService().getAwsLogLevel();
671691
if (this.isValidLogLevel(awsLogLevel)) {
@@ -754,6 +774,19 @@ class Logger extends Utility implements LoggerInterface {
754774
);
755775
}
756776

777+
/**
778+
* Check if a given key is reserved and warn the user if it is.
779+
*
780+
* @param key - The key to check
781+
*/
782+
#checkReservedKeyAndWarn(key: string): boolean {
783+
if (ReservedKeys.includes(key)) {
784+
this.warn(`The key "${key}" is a reserved key and will be dropped.`);
785+
return true;
786+
}
787+
return false;
788+
}
789+
757790
/**
758791
* Get the custom config service, an abstraction used to fetch environment variables.
759792
*/

packages/logger/src/constants.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,18 @@ const LogLevelThreshold = {
3737
SILENT: 28,
3838
} as const;
3939

40-
export { LogJsonIndent, LogLevel, LogLevelThreshold };
40+
/**
41+
* Reserved keys that are included in every log item when using the default log formatter.
42+
*
43+
* These keys are reserved and cannot be overwritten by custom log attributes.
44+
*/
45+
const ReservedKeys = [
46+
'level',
47+
'message',
48+
'sampling_rate',
49+
'service',
50+
'timestamp',
51+
'xray_trace_id',
52+
];
53+
54+
export { LogJsonIndent, LogLevel, LogLevelThreshold, ReservedKeys };

packages/logger/src/formatter/LogItem.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class LogItem {
2323
* @param params - The parameters for the LogItem.
2424
*/
2525
public constructor(params: { attributes: LogAttributes }) {
26-
this.addAttributes(params.attributes);
26+
this.attributes = params.attributes;
2727
}
2828

2929
/**
@@ -32,7 +32,7 @@ class LogItem {
3232
* @param attributes - The attributes to add to the log item.
3333
*/
3434
public addAttributes(attributes: LogAttributes): this {
35-
merge(this.attributes, attributes);
35+
merge(attributes, this.attributes);
3636

3737
return this;
3838
}
@@ -50,7 +50,7 @@ class LogItem {
5050
* This operation removes empty keys from the log item, see {@link removeEmptyKeys | removeEmptyKeys()} for more information.
5151
*/
5252
public prepareForPrint(): void {
53-
this.setAttributes(this.removeEmptyKeys(this.getAttributes()));
53+
this.attributes = this.removeEmptyKeys(this.getAttributes());
5454
}
5555

5656
/**
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { expect, it, vi } from 'vitest';
2+
import { Logger } from '../../src/Logger.js';
3+
4+
vi.hoisted(() => {
5+
process.env.POWERTOOLS_DEV = 'true';
6+
});
7+
8+
it('does not overwrite via appendKeys', () => {
9+
const logger = new Logger({
10+
logLevel: 'DEBUG',
11+
});
12+
const debugSpy = vi.spyOn(console, 'debug');
13+
const warnSpy = vi.spyOn(console, 'warn');
14+
15+
logger.appendKeys({
16+
level: 'Hello, World!',
17+
});
18+
19+
logger.debug('stuff');
20+
21+
const log = JSON.parse(debugSpy.mock.calls[0][0]);
22+
expect(log).toStrictEqual(
23+
expect.objectContaining({
24+
level: 'DEBUG',
25+
})
26+
);
27+
expect(warnSpy).toHaveBeenCalledTimes(1);
28+
const warn = JSON.parse(warnSpy.mock.calls[0][0]);
29+
expect(warn).toStrictEqual(
30+
expect.objectContaining({
31+
message: 'The key "level" is a reserved key and will be dropped.',
32+
})
33+
);
34+
});
35+
36+
it('does not overwrite via appendPersistentKeys', () => {
37+
const logger = new Logger({
38+
logLevel: 'DEBUG',
39+
});
40+
const debugSpy = vi.spyOn(console, 'debug');
41+
const warnSpy = vi.spyOn(console, 'warn');
42+
43+
logger.appendPersistentKeys({
44+
level: 'Hello, World!',
45+
});
46+
47+
logger.debug('stuff');
48+
49+
const log = JSON.parse(debugSpy.mock.calls[0][0]);
50+
expect(log).toStrictEqual(
51+
expect.objectContaining({
52+
level: 'DEBUG',
53+
})
54+
);
55+
expect(warnSpy).toHaveBeenCalledTimes(1);
56+
const warn = JSON.parse(warnSpy.mock.calls[0][0]);
57+
expect(warn).toStrictEqual(
58+
expect.objectContaining({
59+
message: 'The key "level" is a reserved key and will be dropped.',
60+
})
61+
);
62+
});
63+
64+
it('does not overwrite via constructor keys', () => {
65+
const debugSpy = vi.spyOn(console, 'debug');
66+
const warnSpy = vi.spyOn(console, 'warn');
67+
const logger = new Logger({
68+
logLevel: 'DEBUG',
69+
persistentKeys: {
70+
level: 'Hello, World!',
71+
},
72+
});
73+
74+
logger.debug('stuff');
75+
76+
const log = JSON.parse(debugSpy.mock.calls[0][0]);
77+
expect(log).toStrictEqual(
78+
expect.objectContaining({
79+
level: 'DEBUG',
80+
})
81+
);
82+
expect(warnSpy).toHaveBeenCalledTimes(1);
83+
const warn = JSON.parse(warnSpy.mock.calls[0][0]);
84+
expect(warn).toStrictEqual(
85+
expect.objectContaining({
86+
message: 'The key "level" is a reserved key and will be dropped.',
87+
})
88+
);
89+
});
90+
91+
it('does not overwrite via extra keys', () => {
92+
const debugSpy = vi.spyOn(console, 'debug');
93+
const warnSpy = vi.spyOn(console, 'warn');
94+
const logger = new Logger({
95+
logLevel: 'DEBUG',
96+
});
97+
98+
logger.debug('stuff', {
99+
level: 'Hello, World!',
100+
timestamp: 'foo',
101+
message: 'bar',
102+
});
103+
104+
const log = JSON.parse(debugSpy.mock.calls[0][0]);
105+
expect(log).toStrictEqual(
106+
expect.objectContaining({
107+
level: 'DEBUG',
108+
timestamp: expect.not.stringMatching('foo'),
109+
message: 'stuff',
110+
})
111+
);
112+
expect(warnSpy).toHaveBeenCalledTimes(3);
113+
const warn = JSON.parse(warnSpy.mock.calls[0][0]);
114+
expect(warn).toStrictEqual(
115+
expect.objectContaining({
116+
message: 'The key "level" is a reserved key and will be dropped.',
117+
})
118+
);
119+
});

0 commit comments

Comments
 (0)