diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 3a8859e447..d454d50a09 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -640,8 +640,8 @@ class Logger extends Utility implements LoggerInterface { item instanceof Error ? { error: item } : typeof item === 'string' - ? { extra: item } - : item; + ? { extra: item } + : item; additionalLogAttributes = merge(additionalLogAttributes, attributes); }); diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts new file mode 100644 index 0000000000..0fe1dd9909 --- /dev/null +++ b/packages/logger/src/formatter/LogFormatterInterface.ts @@ -0,0 +1,29 @@ +import { LogAttributes, UnformattedAttributes } from '../types'; +import { LogItem } from '../log'; + +/** + * @interface + */ +interface LogFormatterInterface { + /** + * It formats key-value pairs of log attributes. + * + * @param {UnformattedAttributes} attributes + * @param {LogAttributes} additionalLogAttributes + * @returns {LogItem} + */ + formatAttributes( + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem; + + /** + * It formats a given Error parameter. + * + * @param {Error} error + * @returns {LogAttributes} + */ + formatError(error: Error): LogAttributes; +} + +export { LogFormatterInterface }; diff --git a/packages/logger/src/formatter/index.ts b/packages/logger/src/formatter/index.ts new file mode 100644 index 0000000000..ef5d7b16d8 --- /dev/null +++ b/packages/logger/src/formatter/index.ts @@ -0,0 +1,3 @@ +export * from './LogFormatter'; +export * from './LogFormatterInterface'; +export * from './PowertoolsLogFormatter'; diff --git a/packages/logger/src/types/formats/PowertoolsLog.ts b/packages/logger/src/types/formats/PowertoolsLog.ts new file mode 100644 index 0000000000..fa360fef59 --- /dev/null +++ b/packages/logger/src/types/formats/PowertoolsLog.ts @@ -0,0 +1,93 @@ +import type { LogAttributes, LogLevel } from '..'; + +type PowertoolsLog = LogAttributes & { + /** + * timestamp + * + * Description: Timestamp of actual log statement. + * Example: "2020-05-24 18:17:33,774" + */ + timestamp?: string; + + /** + * level + * + * Description: Logging level + * Example: "INFO" + */ + level?: LogLevel; + + /** + * service + * + * Description: Service name defined. + * Example: "payment" + */ + service: string; + + /** + * sampling_rate + * + * Description: The value of the logging sampling rate in percentage. + * Example: 0.1 + */ + sampling_rate?: number; + + /** + * message + * + * Description: Log statement value. Unserializable JSON values will be cast to string. + * Example: "Collecting payment" + */ + message?: string; + + /** + * xray_trace_id + * + * Description: X-Ray Trace ID when Lambda function has enabled Tracing. + * Example: "1-5759e988-bd862e3fe1be46a994272793" + */ + xray_trace_id?: string; + + /** + * cold_start + * + * Description: Indicates whether the current execution experienced a cold start. + * Example: false + */ + cold_start?: boolean; + + /** + * lambda_function_name + * + * Description: The name of the Lambda function. + * Example: "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + */ + lambda_function_name?: string; + + /** + * lambda_function_memory_size + * + * Description: The memory size of the Lambda function. + * Example: 128 + */ + lambda_function_memory_size?: number; + + /** + * lambda_function_arn + * + * Description: The ARN of the Lambda function. + * Example: "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + */ + lambda_function_arn?: string; + + /** + * lambda_request_id + * + * Description: The request ID of the current invocation. + * Example: "899856cb-83d1-40d7-8611-9e78f15f32f4" + */ + lambda_request_id?: string; +}; + +export type { PowertoolsLog }; diff --git a/packages/logger/src/types/formats/index.ts b/packages/logger/src/types/formats/index.ts new file mode 100644 index 0000000000..5a828a385f --- /dev/null +++ b/packages/logger/src/types/formats/index.ts @@ -0,0 +1 @@ +export * from './PowertoolsLog'; diff --git a/packages/logger/tests/unit/helpers.test.ts b/packages/logger/tests/unit/helpers.test.ts new file mode 100644 index 0000000000..5b45837a7e --- /dev/null +++ b/packages/logger/tests/unit/helpers.test.ts @@ -0,0 +1,453 @@ +/** + * Test Logger helpers + * + * @group unit/logger/all + */ +import { Console } from 'console'; +import { + ConfigServiceInterface, + EnvironmentVariablesService, +} from '../../src/config'; +import { LogFormatter, PowertoolsLogFormatter } from '../../src/formatter'; +import { ConstructorOptions, LogLevelThresholds } from '../../src/types'; +import { createLogger, Logger } from './../../src'; + +describe('Helper: createLogger function', () => { + const ENVIRONMENT_VARIABLES = process.env; + const logLevelThresholds: LogLevelThresholds = { + DEBUG: 8, + INFO: 12, + WARN: 16, + ERROR: 20, + CRITICAL: 24, + SILENT: 28, + }; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...ENVIRONMENT_VARIABLES }; + }); + + afterAll(() => { + process.env = ENVIRONMENT_VARIABLES; + }); + + describe('LoggerOptions constructor parameters', () => { + test('when no constructor parameters are set, returns a Logger instance with the options set in the environment variables', () => { + // Prepare + const loggerOptions = undefined; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + defaultServiceName: 'service_undefined', + logLevel: 8, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when no parameters are set, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'WARN', + serviceName: 'my-lambda-service', + sampleRateValue: 1, + logFormatter: new PowertoolsLogFormatter(), + customConfigService: new EnvironmentVariablesService(), + persistentLogAttributes: { + awsAccountId: '123456789', + }, + environment: 'prod', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + defaultServiceName: 'service_undefined', + customConfigService: expect.any(EnvironmentVariablesService), + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 16, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: true, + persistentLogAttributes: { + awsAccountId: '123456789', + }, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: 'prod', + sampleRateValue: 1, + serviceName: 'my-lambda-service', + }, + }); + }); + + test('when no constructor parameters and no environment variables are set, returns a Logger instance with the default properties', () => { + // Prepare + const loggerOptions = undefined; + delete process.env.POWERTOOLS_SERVICE_NAME; + delete process.env.POWERTOOLS_LOG_LEVEL; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + customConfigService: undefined, + defaultServiceName: 'service_undefined', + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 12, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: '', + sampleRateValue: undefined, + serviceName: 'service_undefined', + }, + }); + }); + + test('when a custom logFormatter is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logFormatter: expect.any(LogFormatter), + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: expect.any(LogFormatter), + }) + ); + }); + + test('when a custom serviceName is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + serviceName: 'my-backend-service', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'my-backend-service', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom uppercase logLevel is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'ERROR', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 20, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when a custom lowercase logLevel is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'warn', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 16, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when no log level is set, returns a Logger instance with INFO level', () => { + // Prepare + const loggerOptions: ConstructorOptions = {}; + delete process.env.POWERTOOLS_LOG_LEVEL; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + customConfigService: undefined, + defaultServiceName: 'service_undefined', + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 12, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: '', + sampleRateValue: undefined, + serviceName: 'hello-world', + }, + }); + }); + + test('when a custom sampleRateValue is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + sampleRateValue: 1, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: true, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: 1, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom customConfigService is passed, returns a Logger instance with the correct properties', () => { + const configService: ConfigServiceInterface = { + get(name: string): string { + return `a-string-from-${name}`; + }, + getAwsLogLevel(): string { + return 'INFO'; + }, + getCurrentEnvironment(): string { + return 'dev'; + }, + getLogEvent(): boolean { + return true; + }, + getLogLevel(): string { + return 'INFO'; + }, + getSampleRateValue(): number | undefined { + return undefined; + }, + getServiceName(): string { + return 'my-backend-service'; + }, + isDevMode(): boolean { + return false; + }, + isValueTrue(): boolean { + return true; + }, + }; + // Prepare + const loggerOptions: ConstructorOptions = { + customConfigService: configService, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: 'dev', + serviceName: 'my-backend-service', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: configService, + logLevel: 12, + logFormatter: {}, + }) + ); + }); + + test('when custom persistentLogAttributes is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + persistentLogAttributes: { + aws_account_id: '123456789012', + aws_region: 'eu-west-1', + logger: { + name: 'aws-lambda-powertool-typescript', + version: '0.2.4', + }, + }, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: { + aws_account_id: '123456789012', + aws_region: 'eu-west-1', + logger: { + name: 'aws-lambda-powertool-typescript', + version: '0.2.4', + }, + }, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom environment is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + environment: 'dev', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: 'dev', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + }); +});