Skip to content

Commit f0bdf3c

Browse files
authored
fix(logger): prevent overwriting standard keys (#3553)
1 parent 188b5b7 commit f0bdf3c

File tree

7 files changed

+331
-83
lines changed

7 files changed

+331
-83
lines changed

docs/core/logger.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ These settings will be used across all logs emitted:
5252
| **Logging level** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | `POWERTOOLS_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, `SILENT` | `ERROR` | `logLevel` |
5353
| **Sample rate** | Probability that a Lambda invocation will print all the log items regardless of the log level setting | `POWERTOOLS_LOGGER_SAMPLE_RATE` | `0` | `0.0` to `1.0` | `0.1` | `sampleRateValue` |
5454

55+
???+ info
56+
When `POWERTOOLS_DEV` environment variable is present and set to `"true"` or `"1"`, Logger will pretty-print log messages for easier readability. We recommend to use this setting only when debugging on local environments.
57+
5558
See all environment variables in the [Environment variables](../index.md/#environment-variables) section.
5659
Check API docs to learn more about [Logger constructor options](https://docs.powertools.aws.dev/lambda/typescript/latest/api/types/_aws_lambda_powertools_logger.types.ConstructorOptions.html){target="_blank"}.
5760

@@ -91,8 +94,8 @@ Your Logger will include the following keys to your structured logging (default
9194
| **xray_trace_id**: `string` | `1-5759e988-bd862e3fe1be46a994272793` | X-Ray Trace ID. This value is always presented in Lambda environment, whether [tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html){target="_blank"} or not. Logger will always log this value. |
9295
| **error**: `Object` | `{ name: "Error", location: "/my-project/handler.ts:18", message: "Unexpected error #1", stack: "[stacktrace]"}` | Optional - An object containing information about the Error passed to the logger |
9396

94-
???+ info
95-
When `POWERTOOLS_DEV` environment variable is present and set to `"true"` or `"1"`, Logger will pretty-print log messages for easier readability. We recommend to use this setting only when debugging on local environments.
97+
???+ note
98+
If you emit a log message with a key that matches one of `level`, `message`, `sampling_rate`, `service`, or `timestamp`, the Logger will log a warning message and ignore the key.
9699

97100
### Capturing Lambda context info
98101

@@ -211,6 +214,8 @@ You can append additional keys using either mechanism:
211214
* Append **temporary keys** to all future log messages via the `appendKeys()` method until `resetKeys()` is called
212215
* Set **Persistent keys** for the logger instance via the `persistentKeys` constructor option or the `appendPersistentKeys()` method
213216

217+
To prevent you from accidentally overwriting some of the [standard keys](#standard-structured-keys), we will log a warning message and ignore the key if you try to overwrite them.
218+
214219
#### Extra keys
215220

216221
You can append additional data to a single log item by passing objects as additional parameters.

packages/logger/src/Logger.ts

Lines changed: 135 additions & 75 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';
@@ -25,7 +25,11 @@ import type {
2525
LogLevel,
2626
LoggerInterface,
2727
} from './types/Logger.js';
28-
import type { PowertoolsLogData } from './types/logKeys.js';
28+
import type {
29+
LogKeys,
30+
PowertoolsLogData,
31+
UnformattedAttributes,
32+
} from './types/logKeys.js';
2933

3034
/**
3135
* The Logger utility provides an opinionated logger with output structured as JSON for AWS Lambda.
@@ -131,7 +135,7 @@ class Logger extends Utility implements LoggerInterface {
131135
/**
132136
* Temporary log attributes that can be appended with `appendKeys()` method.
133137
*/
134-
private temporaryLogAttributes: LogAttributes = {};
138+
private temporaryLogAttributes: LogKeys = {};
135139
/**
136140
* Buffer used to store logs until the logger is initialized.
137141
*
@@ -205,38 +209,34 @@ class Logger extends Utility implements LoggerInterface {
205209
}
206210

207211
/**
208-
* Add the given persistent attributes (key-value pairs) to all log items generated by this Logger instance.
209-
*
210-
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys()} instead.
211-
*
212-
* @param attributes - The attributes to add to all log items.
212+
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys() `appendPersistentKeys()`} instead.
213213
*/
214-
public addPersistentLogAttributes(attributes: LogAttributes): void {
214+
public addPersistentLogAttributes(attributes: LogKeys): void {
215215
this.appendPersistentKeys(attributes);
216216
}
217217

218218
/**
219219
* Add the given temporary attributes (key-value pairs) to all log items generated by this Logger instance.
220220
*
221+
* If the key already exists in the attributes, it will be overwritten. If the key is one of `level`, `message`, `sampling_rate`,
222+
* `service`, or `timestamp` we will log a warning and drop the value.
223+
*
221224
* @param attributes - The attributes to add to all log items.
222225
*/
223-
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);
226+
public appendKeys(attributes: LogKeys): void {
227+
this.#appendKeys(attributes, 'temp');
228228
}
229229

230230
/**
231231
* Add the given persistent attributes (key-value pairs) to all log items generated by this Logger instance.
232232
*
233+
* If the key already exists in the attributes, it will be overwritten. If the key is one of `level`, `message`, `sampling_rate`,
234+
* `service`, or `timestamp` we will log a warning and drop the value.
235+
*
233236
* @param attributes - The attributes to add to all log items.
234237
*/
235-
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);
238+
public appendPersistentKeys(attributes: LogKeys): void {
239+
this.#appendKeys(attributes, 'persistent');
240240
}
241241

242242
/**
@@ -514,15 +514,13 @@ class Logger extends Utility implements LoggerInterface {
514514

515515
/**
516516
* @deprecated This method is deprecated and will be removed in the future major versions. Use {@link removePersistentKeys()} instead.
517-
*
518-
* @param keys - The keys to remove.
519517
*/
520518
public removePersistentLogAttributes(keys: string[]): void {
521519
this.removePersistentKeys(keys);
522520
}
523521

524522
/**
525-
* Remove all temporary log attributes added with `appendKeys()` method.
523+
* Remove all temporary log attributes added with {@link appendKeys() `appendKeys()`} method.
526524
*/
527525
public resetKeys(): void {
528526
for (const key of Object.keys(this.temporaryLogAttributes)) {
@@ -553,14 +551,9 @@ class Logger extends Utility implements LoggerInterface {
553551
}
554552

555553
/**
556-
* Set the given attributes (key-value pairs) to all log items generated by this Logger instance.
557-
* Note: this replaces the pre-existing value.
558-
*
559-
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys()} instead.
560-
*
561-
* @param attributes - The attributes to set.
554+
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys() `appendPersistentKeys()`} instead.
562555
*/
563-
public setPersistentLogAttributes(attributes: LogAttributes): void {
556+
public setPersistentLogAttributes(attributes: LogKeys): void {
564557
this.persistentLogAttributes = attributes;
565558
}
566559

@@ -666,6 +659,25 @@ class Logger extends Utility implements LoggerInterface {
666659
merge(this.powertoolsLogData, attributes);
667660
}
668661

662+
/**
663+
* Shared logic for adding keys to the logger instance.
664+
*
665+
* @param attributes - The attributes to add to the log item.
666+
* @param type - The type of the attributes to add.
667+
*/
668+
#appendKeys(attributes: LogKeys, type: 'temp' | 'persistent'): void {
669+
for (const attributeKey of Object.keys(attributes)) {
670+
if (this.#checkReservedKeyAndWarn(attributeKey) === false) {
671+
this.#keys.set(attributeKey, type);
672+
}
673+
}
674+
if (type === 'temp') {
675+
merge(this.temporaryLogAttributes, attributes);
676+
} else {
677+
merge(this.persistentLogAttributes, attributes);
678+
}
679+
}
680+
669681
private awsLogLevelShortCircuit(selectedLogLevel?: string): boolean {
670682
const awsLogLevel = this.getEnvVarsService().getAwsLogLevel();
671683
if (this.isValidLogLevel(awsLogLevel)) {
@@ -688,70 +700,118 @@ class Logger extends Utility implements LoggerInterface {
688700

689701
/**
690702
* Create a log item and populate it with the given log level, input, and extra input.
691-
*
692-
* We start with creating an object with base attributes managed by Powertools.
693-
* Then we create a second object with persistent attributes provided by customers either
694-
* directly to the log entry or through initial configuration and `appendKeys` method.
695-
*
696-
* Once we have the two objects, we pass them to the formatter that will apply the desired
697-
* formatting to the log item.
698-
*
699-
* @param logLevel - The log level of the log item to be printed
700-
* @param input - The main input of the log item, this can be a string or an object with additional attributes
701-
* @param extraInput - Additional attributes to be added to the log item
702703
*/
703704
protected createAndPopulateLogItem(
704705
logLevel: number,
705706
input: LogItemMessage,
706707
extraInput: LogItemExtraInput
707708
): LogItem {
708-
let message = '';
709-
let otherInput: { [key: string]: unknown } = {};
710-
if (typeof input === 'string') {
711-
message = input;
712-
} else {
713-
const { message: inputMessage, ...rest } = input;
714-
message = inputMessage;
715-
otherInput = rest;
716-
}
717-
718-
// create base attributes
719709
const unformattedBaseAttributes = {
720710
logLevel: this.getLogLevelNameFromNumber(logLevel),
721711
timestamp: new Date(),
722-
message,
723712
xRayTraceId: this.envVarsService.getXrayTraceId(),
724713
...this.getPowertoolsLogData(),
714+
message: '',
725715
};
716+
const additionalAttributes = this.#createAdditionalAttributes();
717+
718+
this.#processMainInput(
719+
input,
720+
unformattedBaseAttributes,
721+
additionalAttributes
722+
);
723+
this.#processExtraInput(extraInput, additionalAttributes);
724+
725+
return this.getLogFormatter().formatAttributes(
726+
unformattedBaseAttributes,
727+
additionalAttributes
728+
);
729+
}
730+
731+
/**
732+
* Create additional attributes from persistent and temporary keys
733+
*/
734+
#createAdditionalAttributes(): LogAttributes {
735+
const attributes: LogAttributes = {};
726736

727-
const additionalAttributes: LogAttributes = {};
728-
// gradually add additional attributes picking only the last added for each key
729737
for (const [key, type] of this.#keys) {
730-
if (type === 'persistent') {
731-
additionalAttributes[key] = this.persistentLogAttributes[key];
732-
} else {
733-
additionalAttributes[key] = this.temporaryLogAttributes[key];
738+
if (!this.#checkReservedKeyAndWarn(key)) {
739+
attributes[key] =
740+
type === 'persistent'
741+
? this.persistentLogAttributes[key]
742+
: this.temporaryLogAttributes[key];
734743
}
735744
}
736745

737-
// if the main input is not a string, then it's an object with additional attributes, so we merge it
738-
merge(additionalAttributes, otherInput);
739-
// then we merge the extra input attributes (if any)
746+
return attributes;
747+
}
748+
749+
/**
750+
* Process the main input message and add it to the attributes
751+
*/
752+
#processMainInput(
753+
input: LogItemMessage,
754+
baseAttributes: UnformattedAttributes,
755+
additionalAttributes: LogAttributes
756+
): void {
757+
if (typeof input === 'string') {
758+
baseAttributes.message = input;
759+
return;
760+
}
761+
762+
const { message, ...rest } = input;
763+
baseAttributes.message = message;
764+
765+
for (const [key, value] of Object.entries(rest)) {
766+
if (!this.#checkReservedKeyAndWarn(key)) {
767+
additionalAttributes[key] = value;
768+
}
769+
}
770+
}
771+
772+
/**
773+
* Process extra input items and add them to additional attributes
774+
*/
775+
#processExtraInput(
776+
extraInput: LogItemExtraInput,
777+
additionalAttributes: LogAttributes
778+
): void {
740779
for (const item of extraInput) {
741-
const attributes: LogAttributes =
742-
item instanceof Error
743-
? { error: item }
744-
: typeof item === 'string'
745-
? { extra: item }
746-
: item;
747-
748-
merge(additionalAttributes, attributes);
780+
if (item instanceof Error) {
781+
additionalAttributes.error = item;
782+
} else if (typeof item === 'string') {
783+
additionalAttributes.extra = item;
784+
} else {
785+
this.#processExtraObject(item, additionalAttributes);
786+
}
749787
}
788+
}
750789

751-
return this.getLogFormatter().formatAttributes(
752-
unformattedBaseAttributes,
753-
additionalAttributes
754-
);
790+
/**
791+
* Process an extra input object and add its properties to additional attributes
792+
*/
793+
#processExtraObject(
794+
item: Record<string, unknown>,
795+
additionalAttributes: LogAttributes
796+
): void {
797+
for (const [key, value] of Object.entries(item)) {
798+
if (!this.#checkReservedKeyAndWarn(key)) {
799+
additionalAttributes[key] = value;
800+
}
801+
}
802+
}
803+
804+
/**
805+
* Check if a given key is reserved and warn the user if it is.
806+
*
807+
* @param key - The key to check
808+
*/
809+
#checkReservedKeyAndWarn(key: string): boolean {
810+
if (ReservedKeys.includes(key)) {
811+
this.warn(`The key "${key}" is a reserved key and will be dropped.`);
812+
return true;
813+
}
814+
return false;
755815
}
756816

757817
/**
@@ -1090,7 +1150,7 @@ class Logger extends Utility implements LoggerInterface {
10901150
private setPowertoolsLogData(
10911151
serviceName?: ConstructorOptions['serviceName'],
10921152
environment?: ConstructorOptions['environment'],
1093-
persistentKeys: ConstructorOptions['persistentKeys'] = {}
1153+
persistentKeys?: ConstructorOptions['persistentKeys']
10941154
): void {
10951155
this.addToPowertoolsLogData({
10961156
awsRegion: this.getEnvVarsService().getAwsRegion(),
@@ -1104,7 +1164,7 @@ class Logger extends Utility implements LoggerInterface {
11041164
this.getEnvVarsService().getServiceName() ||
11051165
this.getDefaultServiceName(),
11061166
});
1107-
this.appendPersistentKeys(persistentKeys);
1167+
persistentKeys && this.appendPersistentKeys(persistentKeys);
11081168
}
11091169
}
11101170

packages/logger/src/constants.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,17 @@ 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+
];
52+
53+
export { LogJsonIndent, LogLevel, LogLevelThreshold, ReservedKeys };

packages/logger/src/formatter/LogItem.ts

Lines changed: 2 additions & 2 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.setAttributes(params.attributes);
2727
}
2828

2929
/**
@@ -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
/**

0 commit comments

Comments
 (0)