Skip to content

fix(logger): prevent overwriting standard keys #3553

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 11 commits into from
Feb 5, 2025
183 changes: 127 additions & 56 deletions packages/logger/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
import type { Context, Handler } from 'aws-lambda';
import merge from 'lodash.merge';
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
import { LogJsonIndent, LogLevelThreshold } from './constants.js';
import { LogJsonIndent, LogLevelThreshold, ReservedKeys } from './constants.js';
import type { LogFormatter } from './formatter/LogFormatter.js';
import type { LogItem } from './formatter/LogItem.js';
import { PowertoolsLogFormatter } from './formatter/PowertoolsLogFormatter.js';
Expand All @@ -25,7 +25,10 @@ import type {
LogLevel,
LoggerInterface,
} from './types/Logger.js';
import type { PowertoolsLogData } from './types/logKeys.js';
import type {
PowertoolsLogData,
UnformattedAttributes,
} from './types/logKeys.js';

/**
* The Logger utility provides an opinionated logger with output structured as JSON for AWS Lambda.
Expand Down Expand Up @@ -221,10 +224,7 @@ class Logger extends Utility implements LoggerInterface {
* @param attributes - The attributes to add to all log items.
*/
public appendKeys(attributes: LogAttributes): void {
for (const attributeKey of Object.keys(attributes)) {
this.#keys.set(attributeKey, 'temp');
}
merge(this.temporaryLogAttributes, attributes);
this.#appendKeys(attributes, 'temp');
}

/**
Expand All @@ -233,10 +233,7 @@ class Logger extends Utility implements LoggerInterface {
* @param attributes - The attributes to add to all log items.
*/
public appendPersistentKeys(attributes: LogAttributes): void {
for (const attributeKey of Object.keys(attributes)) {
this.#keys.set(attributeKey, 'persistent');
}
merge(this.persistentLogAttributes, attributes);
this.#appendKeys(attributes, 'persistent');
}

/**
Expand Down Expand Up @@ -666,6 +663,25 @@ class Logger extends Utility implements LoggerInterface {
merge(this.powertoolsLogData, attributes);
}

/**
* Shared logic for adding keys to the logger instance.
*
* @param attributes - The attributes to add to the log item.
* @param type - The type of the attributes to add.
*/
#appendKeys(attributes: LogAttributes, type: 'temp' | 'persistent'): void {
for (const attributeKey of Object.keys(attributes)) {
if (this.#checkReservedKeyAndWarn(attributeKey) === false) {
this.#keys.set(attributeKey, type);
}
}
if (type === 'temp') {
merge(this.temporaryLogAttributes, attributes);
} else {
merge(this.persistentLogAttributes, attributes);
}
}

private awsLogLevelShortCircuit(selectedLogLevel?: string): boolean {
const awsLogLevel = this.getEnvVarsService().getAwsLogLevel();
if (this.isValidLogLevel(awsLogLevel)) {
Expand All @@ -688,70 +704,125 @@ class Logger extends Utility implements LoggerInterface {

/**
* Create a log item and populate it with the given log level, input, and extra input.
*
* We start with creating an object with base attributes managed by Powertools.
* Then we create a second object with persistent attributes provided by customers either
* directly to the log entry or through initial configuration and `appendKeys` method.
*
* Once we have the two objects, we pass them to the formatter that will apply the desired
* formatting to the log item.
*
* @param logLevel - The log level of the log item to be printed
* @param input - The main input of the log item, this can be a string or an object with additional attributes
* @param extraInput - Additional attributes to be added to the log item
*/
protected createAndPopulateLogItem(
logLevel: number,
input: LogItemMessage,
extraInput: LogItemExtraInput
): LogItem {
let message = '';
let otherInput: { [key: string]: unknown } = {};
if (typeof input === 'string') {
message = input;
} else {
const { message: inputMessage, ...rest } = input;
message = inputMessage;
otherInput = rest;
}
const unformattedBaseAttributes = this.#createBaseAttributes(logLevel);
const additionalAttributes = this.#createAdditionalAttributes();

this.#processMainInput(
input,
unformattedBaseAttributes,
additionalAttributes
);
this.#processExtraInput(extraInput, additionalAttributes);

// create base attributes
const unformattedBaseAttributes = {
return this.getLogFormatter().formatAttributes(
unformattedBaseAttributes,
additionalAttributes
);
}

/**
* Create the base attributes for a log item
*/
#createBaseAttributes(logLevel: number): UnformattedAttributes {
return {
logLevel: this.getLogLevelNameFromNumber(logLevel),
timestamp: new Date(),
message,
xRayTraceId: this.envVarsService.getXrayTraceId(),
...this.getPowertoolsLogData(),
message: '',
};
}

/**
* Create additional attributes from persistent and temporary keys
*/
#createAdditionalAttributes(): LogAttributes {
const attributes: LogAttributes = {};

const additionalAttributes: LogAttributes = {};
// gradually add additional attributes picking only the last added for each key
for (const [key, type] of this.#keys) {
if (type === 'persistent') {
additionalAttributes[key] = this.persistentLogAttributes[key];
} else {
additionalAttributes[key] = this.temporaryLogAttributes[key];
if (!this.#checkReservedKeyAndWarn(key)) {
attributes[key] =
type === 'persistent'
? this.persistentLogAttributes[key]
: this.temporaryLogAttributes[key];
}
}

// if the main input is not a string, then it's an object with additional attributes, so we merge it
merge(additionalAttributes, otherInput);
// then we merge the extra input attributes (if any)
return attributes;
}

/**
* Process the main input message and add it to the attributes
*/
#processMainInput(
input: LogItemMessage,
baseAttributes: UnformattedAttributes,
additionalAttributes: LogAttributes
): void {
if (typeof input === 'string') {
baseAttributes.message = input;
return;
}

const { message, ...rest } = input;
baseAttributes.message = message;

for (const [key, value] of Object.entries(rest)) {
if (!this.#checkReservedKeyAndWarn(key)) {
additionalAttributes[key] = value;
}
}
}

/**
* Process extra input items and add them to additional attributes
*/
#processExtraInput(
extraInput: LogItemExtraInput,
additionalAttributes: LogAttributes
): void {
for (const item of extraInput) {
const attributes: LogAttributes =
item instanceof Error
? { error: item }
: typeof item === 'string'
? { extra: item }
: item;

merge(additionalAttributes, attributes);
if (item instanceof Error) {
additionalAttributes.error = item;
} else if (typeof item === 'string') {
additionalAttributes.extra = item;
} else {
this.#processExtraObject(item, additionalAttributes);
}
}
}

return this.getLogFormatter().formatAttributes(
unformattedBaseAttributes,
additionalAttributes
);
/**
* Process an extra input object and add its properties to additional attributes
*/
#processExtraObject(
item: Record<string, unknown>,
additionalAttributes: LogAttributes
): void {
for (const [key, value] of Object.entries(item)) {
if (!this.#checkReservedKeyAndWarn(key)) {
additionalAttributes[key] = value;
}
}
}

/**
* Check if a given key is reserved and warn the user if it is.
*
* @param key - The key to check
*/
#checkReservedKeyAndWarn(key: string): boolean {
if (ReservedKeys.includes(key)) {
this.warn(`The key "${key}" is a reserved key and will be dropped.`);
return true;
}
return false;
}

/**
Expand Down Expand Up @@ -1090,7 +1161,7 @@ class Logger extends Utility implements LoggerInterface {
private setPowertoolsLogData(
serviceName?: ConstructorOptions['serviceName'],
environment?: ConstructorOptions['environment'],
persistentKeys: ConstructorOptions['persistentKeys'] = {}
persistentKeys?: ConstructorOptions['persistentKeys']
): void {
this.addToPowertoolsLogData({
awsRegion: this.getEnvVarsService().getAwsRegion(),
Expand All @@ -1104,7 +1175,7 @@ class Logger extends Utility implements LoggerInterface {
this.getEnvVarsService().getServiceName() ||
this.getDefaultServiceName(),
});
this.appendPersistentKeys(persistentKeys);
persistentKeys && this.appendPersistentKeys(persistentKeys);
}
}

Expand Down
16 changes: 15 additions & 1 deletion packages/logger/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,18 @@ const LogLevelThreshold = {
SILENT: 28,
} as const;

export { LogJsonIndent, LogLevel, LogLevelThreshold };
/**
* Reserved keys that are included in every log item when using the default log formatter.
*
* These keys are reserved and cannot be overwritten by custom log attributes.
*/
const ReservedKeys = [
'level',
'message',
'sampling_rate',
'service',
'timestamp',
'xray_trace_id',
];

export { LogJsonIndent, LogLevel, LogLevelThreshold, ReservedKeys };
4 changes: 2 additions & 2 deletions packages/logger/src/formatter/LogItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class LogItem {
* @param params - The parameters for the LogItem.
*/
public constructor(params: { attributes: LogAttributes }) {
this.addAttributes(params.attributes);
this.setAttributes(params.attributes);
}

/**
Expand All @@ -50,7 +50,7 @@ class LogItem {
* This operation removes empty keys from the log item, see {@link removeEmptyKeys | removeEmptyKeys()} for more information.
*/
public prepareForPrint(): void {
this.setAttributes(this.removeEmptyKeys(this.getAttributes()));
this.attributes = this.removeEmptyKeys(this.getAttributes());
}

/**
Expand Down
Loading