Skip to content
48 changes: 46 additions & 2 deletions packages/logger/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ class Logger extends Utility implements ClassThatLogs {

private envVarsService?: EnvironmentVariablesService;

private logEvent: boolean = false;

private logFormatter?: LogFormatterInterface;

private logLevel?: LogLevel;
Expand Down Expand Up @@ -222,6 +224,16 @@ class Logger extends Utility implements ClassThatLogs {
this.processLogItem('ERROR', input, extraInput);
}

/**
* It returns a boolean value. True means that the Lambda invocation events
* are printed in the logs.
*
* @returns {boolean}
*/
public getLogEvent(): boolean {
return this.logEvent;
}

/**
* It returns a boolean value, if true all the logs will be printed.
*
Expand Down Expand Up @@ -280,6 +292,9 @@ class Logger extends Utility implements ClassThatLogs {
}

this.addContext(context);
if (options) {
this.logEventIfEnabled(event, options.logEvent);
}

/* eslint-disable @typescript-eslint/no-non-null-assertion */
const result = originalMethod!.apply(target, [ event, context, callback ]);
Expand All @@ -293,6 +308,20 @@ class Logger extends Utility implements ClassThatLogs {
};
}

/**
* Logs a Lambda invocation event, if it *should*.
*
** @param {unknown} event
* @param {boolean} [overwriteValue]
* @returns {void}
*/
public logEventIfEnabled(event: unknown, overwriteValue?: boolean): void {
if (!this.shouldLogEvent(overwriteValue)) {
return;
}
this.info('Lambda invocation event', { event });
}

/**
* If the sample rate feature is enabled, the calculation that determines whether the logs
* will actually be printed or not for this invocation is done when the Logger class is
Expand Down Expand Up @@ -353,6 +382,21 @@ class Logger extends Utility implements ClassThatLogs {
this.getEnvVarsService().getSampleRateValue();
}

/**
* It checks whether the current Lambda invocation event should be printed in the logs or not.
*
* @private
* @param {boolean} [overwriteValue]
* @returns {boolean}
*/
public shouldLogEvent(overwriteValue?: boolean): boolean {
if (typeof overwriteValue === 'boolean') {
return overwriteValue;
}

return this.getLogEvent();
}

/**
* It prints a log item with level WARN.
*
Expand Down Expand Up @@ -486,10 +530,10 @@ class Logger extends Utility implements ClassThatLogs {

/**
* It returns the current X-Ray Trace ID parsing the content of the `_X_AMZN_TRACE_ID` env variable.
*
*
* The X-Ray Trace data available in the environment variable has this format:
* `Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1`,
*
*
* The actual Trace ID is: `1-5759e988-bd862e3fe1be46a994272793`.
*
* @private
Expand Down
19 changes: 19 additions & 0 deletions packages/logger/src/config/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ abstract class ConfigService implements ConfigServiceInterface {
* @protected
*/
protected currentEnvironmentVariable = 'ENVIRONMENT';
protected logEventVariable = 'POWERTOOLS_LOGGER_LOG_EVENT';
protected logLevelVariable = 'LOG_LEVEL';
protected sampleRateValueVariable = 'POWERTOOLS_LOGGER_SAMPLE_RATE';
protected serviceNameVariable = 'POWERTOOLS_SERVICE_NAME';
Expand All @@ -38,6 +39,13 @@ abstract class ConfigService implements ConfigServiceInterface {
*/
public abstract getCurrentEnvironment(): string;

/**
* It returns the value of the POWERTOOLS_LOGGER_LOG_EVENT environment variable.
*
* @returns {boolean}
*/
public abstract getLogEvent(): boolean;

/**
* It returns the value of the LOG_LEVEL environment variable.
*
Expand All @@ -59,6 +67,17 @@ abstract class ConfigService implements ConfigServiceInterface {
*/
public abstract getServiceName(): string;

/**
* It returns true if the string value represents a boolean true value.
*
* @param {string} value
* @returns boolean
* @protected
*/
protected isValueTrue(value: string): boolean {
return value.toLowerCase() === 'true' || value === '1';
}

}

export {
Expand Down
7 changes: 7 additions & 0 deletions packages/logger/src/config/ConfigServiceInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ interface ConfigServiceInterface {
*/
getCurrentEnvironment(): string

/**
* It returns the value of the POWERTOOLS_LOGGER_LOG_EVENT environment variable.
*
* @returns {boolean}
*/
getLogEvent(): boolean

/**
* It returns the value of the LOG_LEVEL environment variable.
*
Expand Down
9 changes: 9 additions & 0 deletions packages/logger/src/config/EnvironmentVariablesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ class EnvironmentVariablesService extends ConfigService {
return this.get(this.functionVersionVariable);
}

/**
* It returns the value of the POWERTOOLS_LOGGER_LOG_EVENT environment variable.
*
* @returns {boolean}
*/
public getLogEvent(): boolean {
return this.isValueTrue(this.get(this.logEventVariable));
}

/**
* It returns the value of the LOG_LEVEL environment variable.
*
Expand Down
5 changes: 4 additions & 1 deletion packages/logger/src/middleware/middy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const injectLambdaContext = (target: Logger | Logger[], options?: HandlerOptions
if (options && options.clearState === true) {
persistentAttributes.push({ ...logger.getPersistentLogAttributes() });
}
if (options) {
logger.logEventIfEnabled(request.event, options.logEvent);
}
});
};

Expand All @@ -48,7 +51,7 @@ const injectLambdaContext = (target: Logger | Logger[], options?: HandlerOptions
});
}
};

return {
before: injectLambdaContextBefore,
after: injectLambdaContextAfter
Expand Down
1 change: 1 addition & 0 deletions packages/logger/src/types/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type ClassThatLogs = {
};

type HandlerOptions = {
logEvent?: boolean
clearState?: boolean
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Context, APIGatewayAuthorizerResult } from 'aws-lambda';
import middy from '@middy/core';

const PERSISTENT_KEY = process.env.PERSISTENT_KEY;
const PERSISTENT_KEY_FIRST_INVOCATION_ONLY = process.env.PERSISTENT_KEY_FIRST_INVOCATION_ONLY;
const PERSISTENT_VALUE = process.env.PERSISTENT_VALUE;
const REMOVABLE_KEY = process.env.REMOVABLE_KEY;
const REMOVABLE_VALUE = process.env.REMOVABLE_VALUE;
Expand Down Expand Up @@ -33,7 +34,7 @@ const testFunction = async (event: LambdaEvent, context: Context): Promise<{requ
const specialValue = event.invocation;
if (specialValue === 0) {
logger.appendKeys({
specialKey: specialValue
[PERSISTENT_KEY_FIRST_INVOCATION_ONLY]: specialValue
});
}

Expand Down Expand Up @@ -72,4 +73,5 @@ const testFunction = async (event: LambdaEvent, context: Context): Promise<{requ
};
};

export const handler = middy(testFunction).use(injectLambdaContext(logger, { clearState: true }));
export const handler = middy(testFunction)
.use(injectLambdaContext(logger, { clearState: true, logEvent: true }));
36 changes: 32 additions & 4 deletions packages/logger/tests/e2e/basicFeatures.middy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const lambdaFunctionCodeFile = 'basicFeatures.middy.test.FunctionCode.ts';

// Text to be used by Logger in the Lambda function
const PERSISTENT_KEY = 'persistentKey';
const PERSISTENT_KEY_FIRST_INVOCATION_ONLY = 'specialKey';
const PERSISTENT_VALUE = `a persistent value that will be put in every log ${uuid}`;
const REMOVABLE_KEY = 'removableKey';
const REMOVABLE_VALUE = `a persistent value that will be removed and not displayed in any log ${uuid}`;
Expand Down Expand Up @@ -73,6 +74,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}

// Text to be used by Logger in the Lambda function
PERSISTENT_KEY,
PERSISTENT_KEY_FIRST_INVOCATION_ONLY,
PERSISTENT_VALUE,
REMOVABLE_KEY,
REMOVABLE_VALUE,
Expand Down Expand Up @@ -127,9 +129,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}
expect(message).not.toContain(`"cold_start":true`);
}
}, TEST_CASE_TIMEOUT);
});

describe('Context data', () => {
it('should log context information in every log', async () => {
const logMessages = invocationLogs[0].getFunctionLogs();

Expand All @@ -143,6 +143,34 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}
}, TEST_CASE_TIMEOUT);
});

describe('Log event', () => {

it('should log the event on the first invocation', async () => {
const firstInvocationMessages = invocationLogs[0].getAllFunctionLogs();
let eventLoggedInFirstInvocation = false;
for (const message of firstInvocationMessages) {
if (message.includes(`event`)) {
eventLoggedInFirstInvocation = true;
expect(message).toContain(`"event":{"invocation":0}`);
}
}

const secondInvocationMessages = invocationLogs[1].getAllFunctionLogs();
let eventLoggedInSecondInvocation = false;
for (const message of secondInvocationMessages) {
if (message.includes(`event`)) {
eventLoggedInSecondInvocation = true;
expect(message).toContain(`"event":{"invocation":1}`);
}
}

expect(eventLoggedInFirstInvocation).toBe(true);
expect(eventLoggedInSecondInvocation).toBe(true);

}, TEST_CASE_TIMEOUT);

});

describe('Persistent additional log keys and values', () => {
it('should contain persistent value in every log', async () => {
const logMessages = invocationLogs[0].getFunctionLogs();
Expand All @@ -163,12 +191,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}
it('with clear state enabled, should not persist keys across invocations', async () => {
const firstInvocationMessages = invocationLogs[0].getFunctionLogs();
for (const message of firstInvocationMessages) {
expect(message).toContain(`"specialKey":0`);
expect(message).toContain(`"${PERSISTENT_KEY_FIRST_INVOCATION_ONLY}":0`);
}

const secondInvocationMessages = invocationLogs[1].getFunctionLogs();
for (const message of secondInvocationMessages) {
expect(message).not.toContain(`"specialKey":0`);
expect(message).not.toContain(`"${PERSISTENT_KEY_FIRST_INVOCATION_ONLY}":0`);
}
}, TEST_CASE_TIMEOUT);
});
Expand Down
50 changes: 50 additions & 0 deletions packages/logger/tests/unit/Logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ describe('Class: Logger', () => {
coldStart: false, // This is now false because the `coldStart` attribute has been already accessed once by the `addContext` method
customConfigService: undefined,
envVarsService: expect.any(EnvironmentVariablesService),
logEvent: false,
logFormatter: expect.any(PowertoolLogFormatter),
logLevel: 'DEBUG',
logLevelThresholds: {
Expand Down Expand Up @@ -808,6 +809,7 @@ describe('Class: Logger', () => {
test('when used as decorator, it returns a function with the correct scope of the decorated class', async () => {

// Prepare

const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], 'info').mockImplementation();
class LambdaFunction implements LambdaInterface {
Expand Down Expand Up @@ -1042,6 +1044,7 @@ describe('Class: Logger', () => {
coldStart: true,
customConfigService: undefined,
envVarsService: expect.any(EnvironmentVariablesService),
logEvent: false,
logFormatter: expect.any(PowertoolLogFormatter),
logLevel: 'DEBUG',
logLevelThresholds: {
Expand All @@ -1065,6 +1068,7 @@ describe('Class: Logger', () => {
coldStart: true,
customConfigService: undefined,
envVarsService: expect.any(EnvironmentVariablesService),
logEvent: false,
logFormatter: expect.any(PowertoolLogFormatter),
logLevel: 'DEBUG',
logLevelThresholds: {
Expand All @@ -1090,6 +1094,7 @@ describe('Class: Logger', () => {
coldStart: true,
customConfigService: undefined,
envVarsService: expect.any(EnvironmentVariablesService),
logEvent: false,
logFormatter: expect.any(PowertoolLogFormatter),
logLevel: 'DEBUG',
logLevelThresholds: {
Expand All @@ -1113,6 +1118,7 @@ describe('Class: Logger', () => {
coldStart: true,
customConfigService: undefined,
envVarsService: expect.any(EnvironmentVariablesService),
logEvent: false,
logFormatter: expect.any(PowertoolLogFormatter),
logLevel: 'ERROR',
logLevelThresholds: {
Expand All @@ -1135,4 +1141,48 @@ describe('Class: Logger', () => {

});

describe('Method: logEventIfEnabled', () => {

test('When the feature is disabled, it DOES NOT log the event', () => {

// Prepare
const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], 'info').mockImplementation();

// Act
logger.logEventIfEnabled(dummyEvent);

// Assess

expect(consoleSpy).toBeCalledTimes(0);
});

test('When the feature is enabled via overwrite flag, it DOES log the event', () => {

// Prepare
const event = {
something: 'happened!'
};
const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], 'info').mockImplementation();

// Act
logger.logEventIfEnabled(event, true);

// Assess
expect(consoleSpy).toBeCalledTimes(1);
expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({
level: 'INFO',
message: 'Lambda invocation event',
service: 'hello-world',
timestamp: '2016-06-20T12:08:10.000Z',
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
event: {
something: 'happened!'
}
},
));
});
});

});
Loading