diff --git a/.eslintrc.js b/.eslintrc.js index c15fe9b23d..6ffb8193a4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,42 +1,63 @@ module.exports = { env: { - 'jest': true, 'browser': false, + 'es2020': true, + 'jest': true, 'node': true, - 'es2020': true }, - parser: '@typescript-eslint/parser', extends: [ 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended' ], + ignorePatterns: ['tests/resources/*'], + parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], rules: { - 'no-console': 0, - 'semi': [ 'error', 'always' ], - 'newline-before-return': 2, - 'indent': [ 'error', 2, { 'SwitchCase': 1 } ], - 'quotes': [ 'error', 'single', { 'allowTemplateLiterals': true } ], - 'object-curly-spacing': [ 'error', 'always' ], + '@typescript-eslint/ban-ts-ignore': ['off'], + '@typescript-eslint/camelcase': ['off'], + '@typescript-eslint/explicit-function-return-type': [ 'error', { 'allowExpressions': true } ], + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/indent': [ 'error', 2, { 'SwitchCase': 1 } ], + '@typescript-eslint/interface-name-prefix': ['off'], + '@typescript-eslint/member-delimiter-style': [ 'error', { 'multiline': { 'delimiter': 'none' } } ], + '@typescript-eslint/member-ordering': [ 'error', { + 'default': { 'memberTypes': [ + 'signature', + 'public-field', // = ["public-static-field", "public-instance-field"] + 'protected-field', // = ["protected-static-field", "protected-instance-field"] + 'private-field', // = ["private-static-field", "private-instance-field"] + 'constructor', + 'public-method', // = ["public-static-method", "public-instance-method"] + 'protected-method', // = ["protected-static-method", "protected-instance-method"] + 'private-method' // = ["private-static-method", "private-instance-method"] + ] , + 'order': 'alphabetically' } + } ], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-inferrable-types': ['off'], + '@typescript-eslint/no-unused-vars': [ 'error', { 'argsIgnorePattern': '^_' } ], + '@typescript-eslint/no-use-before-define': ['off'], + '@typescript-eslint/semi': [ 'error', 'always' ], 'array-bracket-spacing': [ 'error', 'always', { 'singleValue': false } ], 'arrow-body-style': [ 'error', 'as-needed' ], 'computed-property-spacing': [ 'error', 'never' ], - 'no-multiple-empty-lines': [ 'error', { 'max': 1, 'maxBOF': 0 } ], - 'prefer-arrow-callback': 'error', 'func-style': [ 'warn', 'expression' ], - 'no-multi-spaces': [ 'error', { 'ignoreEOLComments': false } ], + 'indent': [ 'error', 2, { 'SwitchCase': 1 } ], 'keyword-spacing': 'error', - '@typescript-eslint/semi': [ 'error', 'always' ], - '@typescript-eslint/indent': [ 'error', 2, { 'SwitchCase': 1 } ], - '@typescript-eslint/explicit-function-return-type': [ 'error', { 'allowExpressions': true } ], - '@typescript-eslint/member-delimiter-style': [ 'error', { 'multiline': { 'delimiter': 'none' } } ], - '@typescript-eslint/interface-name-prefix': ['off'], - '@typescript-eslint/camelcase': ['off'], - '@typescript-eslint/no-use-before-define': ['off'], - '@typescript-eslint/ban-ts-ignore': ['off'], - '@typescript-eslint/no-inferrable-types': ['off'], - '@typescript-eslint/no-unused-vars': [ 'error', { 'argsIgnorePattern': '^_' } ], - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/explicit-member-accessibility': 'error' + 'newline-before-return': 2, + 'no-console': 0, + 'no-multi-spaces': [ 'error', { 'ignoreEOLComments': false } ], + 'no-multiple-empty-lines': [ 'error', { 'max': 1, 'maxBOF': 0 } ], + 'object-curly-spacing': [ 'error', 'always' ], + 'prefer-arrow-callback': 'error', + 'quotes': [ 'error', 'single', { 'allowTemplateLiterals': true } ], + 'semi': [ 'error', 'always' ], + 'sort-imports': [ 'error', { + 'allowSeparatedGroups': true, + 'ignoreCase': true, + 'ignoreDeclarationSort': false, + 'ignoreMemberSort': true, + 'memberSyntaxSortOrder': [ 'all', 'single', 'multiple', 'none' ] + } ] } }; \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d99dd2744..982356526f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,14 @@ jobs: steps: - uses: actions/checkout@v2 - name: Install packages - run: npm install + run: | + export NODE_ENV=dev + npm ci + npm run lerna-ci - name: Run lint - run: npm run lint + run: npm run lerna-lint - name: Run tests - run: npm run test + run: npm run lerna-test - name: Report Coverage if: ${{ github.event_name == 'pull_request' }} uses: romeovs/lcov-reporter-action@v0.2.11 diff --git a/package-lock.json b/package-lock.json index 04ed64eb91..3d0088480a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/aws-lambda": "^8.10.72", "@types/jest": "^26.0.19", "@types/node": "^14.14.16", "@typescript-eslint/eslint-plugin": "^4.11.1", @@ -4400,6 +4401,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.72", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.72.tgz", + "integrity": "sha512-jOrTwAhSiUtBIN/QsWNKlI4+4aDtpZ0sr2BRvKW6XQZdspgHUSHPcuzxbzCRiHUiDQ+0026u5TSE38VyIhNnfA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", @@ -19856,6 +19863,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "@types/aws-lambda": { + "version": "8.10.72", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.72.tgz", + "integrity": "sha512-jOrTwAhSiUtBIN/QsWNKlI4+4aDtpZ0sr2BRvKW6XQZdspgHUSHPcuzxbzCRiHUiDQ+0026u5TSE38VyIhNnfA==", + "dev": true + }, "@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", diff --git a/package.json b/package.json index faadf613cb..fa21e64e41 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,15 @@ "types": "lib/", "scripts": { "commit": "commit", - "ci": "lerna exec -- npm ci", - "test": "lerna exec -- jest --coverage --detectOpenHandles", - "build": "lerna exec -- tsc", - "lint": "lerna exec -- eslint \"./{src,tests}/**/*.ts\"", - "format": "lerna exec -- eslint --fix \"./{src,tests}/**/*.ts\"", - "prepare": "lerna exec -- npm run build", - "prepublishOnly": "lerna exec -- npm test && lerna exec -- npm run lint", - "preversion": "lerna exec -- npm run lint", - "version": "lerna exec -- npm run format && git add -A src", + "lerna-ci": "lerna exec -- npm ci", + "lerna-test": "lerna exec -- jest --coverage --detectOpenHandles", + "lerna-build": "lerna exec -- tsc", + "lerna-lint": "lerna exec -- eslint \"./{src,tests}/**/*.ts\"", + "lerna-format": "lerna exec -- eslint --fix \"./{src,tests}/**/*.ts\"", + "lerna-prepare": "lerna exec -- npm run build", + "lerna-prepublishOnly": "lerna exec -- npm test && lerna exec -- npm run lint", + "lerna-preversion": "lerna exec -- npm run lint", + "lerna-version": "lerna exec -- npm run format && git add -A src", "postversion": "git push && git push --tags" }, "repository": { @@ -34,6 +34,7 @@ "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/aws-lambda": "^8.10.72", "@types/jest": "^26.0.19", "@types/node": "^14.14.16", "@typescript-eslint/eslint-plugin": "^4.11.1", diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 0000000000..1bd086ee49 --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,372 @@ +# `logger` + +## Usage + +### Getting started + +```typescript +// Import the library +import { Logger } from '../src'; +// When going public, it will be something like: import { Logger } from '@aws-lambda-powertools/logger'; + +// Environment variables set for the Lambda +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +// Instantiate the Logger with default configuration +const logger = new Logger(); + +// Log with different levels +logger.debug('This is a DEBUG log'); +logger.info('This is an INFO log'); +logger.warn('This is a WARN log'); +logger.error('This is an ERROR log'); + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + level: 'WARN', + message: 'This is a WARN log', + service: 'hello-world', + timestamp: '2021-03-13T18:02:49.280Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} +{ + level: 'ERROR', + message: 'This is an ERROR log', + service: 'hello-world', + timestamp: '2021-03-13T18:02:49.282Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} + +``` +
+ + +### Capturing Lambda context info + +```typescript +// Environment variables set for the Lambda +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; +process.env.POWERTOOLS_CONTEXT_ENABLED = 'TRUE'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async (event, context) => { + logger.addContext(context); // This should be in a custom Middy middleware https://github.com/middyjs/middy + + logger.debug('This is a DEBUG log'); + logger.info('This is an INFO log'); + logger.warn('This is a WARN log'); + logger.error('This is an ERROR log'); + + return { + foo: 'bar' + }; + +}; + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + aws_request_id: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', + cold_start: true, + lambda_function_arn: 'arn:aws:lambda:eu-central-1:123456789012:function:Example', + lambda_function_memory_size: 128, + lambda_function_name: 'foo-bar-function', + level: 'WARN', + message: 'This is a WARN log', + service: 'hello-world', + timestamp: '2021-03-13T18:11:46.919Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} +{ + aws_request_id: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', + cold_start: true, + lambda_function_arn: 'arn:aws:lambda:eu-central-1:123456789012:function:Example', + lambda_function_memory_size: 128, + lambda_function_name: 'foo-bar-function', + level: 'ERROR', + message: 'This is an ERROR log', + service: 'hello-world', + timestamp: '2021-03-13T18:11:46.921Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} + +``` +
+ + +### Appending additional keys + +```typescript + +// Environment variables set for the Lambda +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async () => { + + // Pass a custom correlation ID + logger.warn('This is a WARN log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }); + + // Pass an error that occurred + logger.error('This is an ERROR log', new Error('Something bad happened!')); + + return { + foo: 'bar' + }; + +}; + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + level: 'WARN', + message: 'This is a WARN log', + service: 'hello-world', + timestamp: '2021-03-13T20:21:28.423Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456', + correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } +} +{ + level: 'ERROR', + message: 'This is an ERROR log', + service: 'hello-world', + timestamp: '2021-03-13T20:21:28.426Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456', + error: { + name: 'Error', + message: 'Something bad happened!', + stack: 'Error: Something bad happened!\n' + + ' at lambdaHandler (/Users/username/Workspace/projects/aws-lambda-powertools-typescript/packages/logger/examples/additional-keys.ts:22:40)\n' + + ' at Object. (/Users/username/Workspace/projects/aws-lambda-powertools-typescript/packages/logger/examples/additional-keys.ts:30:1)\n' + + ' at Module._compile (node:internal/modules/cjs/loader:1108:14)\n' + + ' at Module.m._compile (/Users/username/Workspace/projects/aws-lambda-powertools-typescript/packages/logger/node_modules/ts-node/src/index.ts:1056:23)\n' + + ' at Module._extensions..js (node:internal/modules/cjs/loader:1137:10)\n' + + ' at Object.require.extensions. [as .ts] (/Users/username/Workspace/projects/aws-lambda-powertools-typescript/packages/logger/node_modules/ts-node/src/index.ts:1059:12)\n' + + ' at Module.load (node:internal/modules/cjs/loader:973:32)\n' + + ' at Function.Module._load (node:internal/modules/cjs/loader:813:14)\n' + + ' at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)\n' + + ' at main (/Users/username/Workspace/projects/aws-lambda-powertools-typescript/packages/logger/node_modules/ts-node/src/bin.ts:198:14)' + } +} +``` +
+ +### Reusing Logger across your code + +```typescript +// Environment variables set for the Lambda +process.env.LOG_LEVEL = 'INFO'; + +const parentLogger = new Logger(); + +const childLogger = parentLogger.createChild({ + logLevel: 'ERROR' +}); + +const lambdaHandler: Handler = async () => { + + parentLogger.info('This is an INFO log, from the parent logger'); + parentLogger.error('This is an ERROR log, from the parent logger'); + + childLogger.info('This is an INFO log, from the child logger'); + childLogger.error('This is an ERROR log, from the child logger'); + + return { + foo: 'bar' + }; + +}; + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + level: 'INFO', + message: 'This is an INFO log, from the parent logger', + service: 'hello-world', + timestamp: '2021-03-13T20:33:41.128Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} +{ + level: 'ERROR', + message: 'This is an ERROR log, from the parent logger', + service: 'hello-world', + timestamp: '2021-03-13T20:33:41.130Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} +{ + level: 'ERROR', + message: 'This is an ERROR log, from the child logger', + service: 'hello-world', + timestamp: '2021-03-13T20:33:41.131Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} + +``` +
+ + +### Sampling debug logs + +```typescript + +// Environment variables set for the Lambda +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async () => { + + logger.info('This is INFO log #1'); + logger.info('This is INFO log #2'); + logger.info('This is INFO log #3'); + logger.info('This is INFO log #4'); + + return { + foo: 'bar' + }; + +}; + + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + level: 'INFO', + message: 'This is INFO log #2', + sampling_rate: 0.5, + service: 'hello-world', + timestamp: '2021-03-13T20:45:06.093Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} +{ + level: 'INFO', + message: 'This is INFO log #4', + sampling_rate: 0.5, + service: 'hello-world', + timestamp: '2021-03-13T20:45:06.096Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' +} + +``` + +
+ +## Custom logger options: log level, service name, sample rate value, log attributes, variables source, log format + +```typescript + +process.env.CUSTOM_ENV = 'prod'; +process.env.POWERTOOLS_CONTEXT_ENABLED = 'TRUE'; + +// Custom configuration service for variables, and custom formatter to comply to different log JSON schema +import { CustomConfigService } from './config/CustomConfigService'; +import { CustomLogFormatter } from './formatters/CustomLogFormatter'; + +const logger = new Logger({ + logLevel: 'INFO', // Override options + serviceName: 'foo-bar', + sampleRateValue: 0.00001, + customAttributes: { // Custom attributes that will be added in every log + awsAccountId: '123456789012', + logger: { + name: powertool.name, + version: powertool.version, + } + }, + logFormatter: new CustomLogFormatter(), // Custom log formatter to print the log in a custom format (JSON schema) + customConfigService: new CustomConfigService() // Custom config service, that could be used for AppConfig for example +}); + +const lambdaHandler: Handler = async (event, context) => { + logger.addContext(context); + + logger.info('This is an INFO log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }); + + return { + foo: 'bar' + }; +}; + + +``` + +
+ Click to expand and see the logs outputs + +```bash + +{ + message: 'This is an INFO log', + service: 'foo-bar', + environment: 'prod', + awsRegion: 'eu-central-1', + correlationIds: { + awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', + xRayTraceId: 'abcdef123456abcdef123456abcdef123456', + myCustomCorrelationId: 'foo-bar-baz' + }, + lambdaFunction: { + name: 'foo-bar-function', + arn: 'arn:aws:lambda:eu-central-1:123456789012:function:Example', + memoryLimitInMB: 128, + version: '$LATEST', + coldStart: true + }, + logLevel: 'INFO', + timestamp: '2021-03-13T21:43:47.759Z', + logger: { + sampleRateValue: 0.00001, + name: 'aws-lambda-powertools-typescript', + version: '0.0.1' + }, + awsAccountId: '123456789012' +} + +``` + +
+ +## Test locally + +```bash + +npm run test + +npm run example:hello-world +npm run example:hello-world-with-context +npm run example:custom-logger-options +npm run example:child-logger + +``` diff --git a/packages/logger/examples/additional-keys.ts b/packages/logger/examples/additional-keys.ts new file mode 100644 index 0000000000..6d5c918052 --- /dev/null +++ b/packages/logger/examples/additional-keys.ts @@ -0,0 +1,30 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async () => { + + // Pass a custom correlation ID + logger.warn('This is a WARN log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }); + + // Pass an error that occurred + logger.error('This is an ERROR log', new Error('Something bad happened!')); + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/logger/examples/child-logger.ts b/packages/logger/examples/child-logger.ts new file mode 100644 index 0000000000..dd54b9a4be --- /dev/null +++ b/packages/logger/examples/child-logger.ts @@ -0,0 +1,33 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.LOG_LEVEL = 'INFO'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const parentLogger = new Logger(); + +const childLogger = parentLogger.createChild({ + logLevel: 'ERROR' +}); + +const lambdaHandler: Handler = async () => { + + parentLogger.info('This is an INFO log, from the parent logger'); + parentLogger.error('This is an ERROR log, from the parent logger'); + + childLogger.info('This is an INFO log, from the child logger'); + childLogger.error('This is an ERROR log, from the child logger'); + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => {}); \ No newline at end of file diff --git a/packages/logger/examples/config/CustomConfigService.ts b/packages/logger/examples/config/CustomConfigService.ts new file mode 100644 index 0000000000..e66203c502 --- /dev/null +++ b/packages/logger/examples/config/CustomConfigService.ts @@ -0,0 +1,16 @@ +import { EnvironmentVariablesService } from '../../src/config'; + +class CustomConfigService extends EnvironmentVariablesService { + + // Custom environment variables + protected customEnvironmentVariable = 'CUSTOM_ENV'; + + public getCurrentEnvironment(): string { + return this.get(this.customEnvironmentVariable); + } + +} + +export { + CustomConfigService +}; \ No newline at end of file diff --git a/packages/logger/examples/custom-logger-options.ts b/packages/logger/examples/custom-logger-options.ts new file mode 100644 index 0000000000..a861bae120 --- /dev/null +++ b/packages/logger/examples/custom-logger-options.ts @@ -0,0 +1,42 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.CUSTOM_ENV = 'prod'; +process.env.POWERTOOLS_CONTEXT_ENABLED = 'TRUE'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import * as powertool from '../../../package.json'; +import { CustomConfigService } from './config/CustomConfigService'; +import { CustomLogFormatter } from './formatters/CustomLogFormatter'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const logger = new Logger({ + logLevel: 'INFO', // Override options + serviceName: 'foo-bar', + sampleRateValue: 0.00001, + customAttributes: { // Custom attributes that will be added in every log + awsAccountId: '123456789012', + logger: { + name: powertool.name, + version: powertool.version, + } + }, + logFormatter: new CustomLogFormatter(), // Custom log formatter to print the log in custom structure + customConfigService: new CustomConfigService() // Custom config service, that could be used for AppConfig for example +}); + +const lambdaHandler: Handler = async (event, context) => { + logger.addContext(context); + + logger.info('This is an INFO log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }); + + return { + foo: 'bar' + }; +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/logger/examples/formatters/CustomLogFormatter.ts b/packages/logger/examples/formatters/CustomLogFormatter.ts new file mode 100644 index 0000000000..8bb0872e3c --- /dev/null +++ b/packages/logger/examples/formatters/CustomLogFormatter.ts @@ -0,0 +1,37 @@ +import { LogFormatter } from '../../src/formatter'; +import { LogAttributes, UnformattedAttributes } from '../../types'; + +type MyCompanyLog = LogAttributes; + +class CustomLogFormatter extends LogFormatter { + + public format(attributes: UnformattedAttributes): MyCompanyLog { + return { + message: attributes.message, + service: attributes.serviceName, + environment: attributes.environment, + awsRegion: attributes.awsRegion, + correlationIds: { + awsRequestId: attributes.lambdaContext?.awsRequestId, + xRayTraceId: attributes.xRayTraceId + }, + lambdaFunction: { + name: attributes.lambdaContext?.name, + arn: attributes.lambdaContext?.arn, + memoryLimitInMB: attributes.lambdaContext?.memoryLimitInMB, + version: attributes.lambdaContext?.version, + coldStart: attributes.lambdaContext?.coldStart, + }, + logLevel: attributes.logLevel, + timestamp: this.formatTimestamp(attributes.timestamp), + logger: { + sampleRateValue: attributes.sampleRateValue, + }, + }; + } + +} + +export { + CustomLogFormatter +}; \ No newline at end of file diff --git a/packages/logger/examples/hello-world-with-context.ts b/packages/logger/examples/hello-world-with-context.ts new file mode 100644 index 0000000000..806cbd9433 --- /dev/null +++ b/packages/logger/examples/hello-world-with-context.ts @@ -0,0 +1,31 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; +process.env.POWERTOOLS_CONTEXT_ENABLED = 'TRUE'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async (event, context) => { + logger.addContext(context); + + logger.debug('This is a DEBUG log'); + logger.info('This is an INFO log'); + logger.warn('This is a WARN log'); + logger.error('This is an ERROR log'); + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/logger/examples/hello-world.ts b/packages/logger/examples/hello-world.ts new file mode 100644 index 0000000000..d922c5adee --- /dev/null +++ b/packages/logger/examples/hello-world.ts @@ -0,0 +1,29 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async () => { + + logger.debug('This is a DEBUG log'); + logger.info('This is an INFO log'); + logger.warn('This is a WARN log'); + logger.error('This is an ERROR log'); + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/logger/examples/sample-rate.ts b/packages/logger/examples/sample-rate.ts new file mode 100644 index 0000000000..59049af7d3 --- /dev/null +++ b/packages/logger/examples/sample-rate.ts @@ -0,0 +1,30 @@ +import { populateEnvironmentVariables } from '../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.LOG_LEVEL = 'WARN'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; +process.env.POWERTOOLS_LOGGER_SAMPLE_RATE = '0.5'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { Handler } from 'aws-lambda'; +import { Logger } from '../src'; + +const logger = new Logger(); + +const lambdaHandler: Handler = async () => { + + logger.info('This is INFO log #1'); + logger.info('This is INFO log #2'); + logger.info('This is INFO log #3'); + logger.info('This is INFO log #4'); + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/logging/jest.config.js b/packages/logger/jest.config.js similarity index 86% rename from packages/logging/jest.config.js rename to packages/logger/jest.config.js index 6fbaae512f..9c42534a7f 100644 --- a/packages/logging/jest.config.js +++ b/packages/logger/jest.config.js @@ -22,10 +22,10 @@ module.exports = { ], 'coverageThreshold': { 'global': { - 'statements': 100, - 'branches': 100, - 'functions': 100, - 'lines': 100, + 'statements': 70, + 'branches': 60, + 'functions': 70, + 'lines': 70, }, }, 'coverageReporters': [ diff --git a/packages/logging/package-lock.json b/packages/logger/package-lock.json similarity index 99% rename from packages/logging/package-lock.json rename to packages/logger/package-lock.json index 4a79077332..3aaec45b25 100644 --- a/packages/logging/package-lock.json +++ b/packages/logger/package-lock.json @@ -1,17 +1,22 @@ { - "name": "@aws-lambda-powertools/logging", + "name": "@aws-lambda-powertools/logger", "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@aws-lambda-powertools/logging", + "name": "@aws-lambda-powertools/logger", "version": "0.0.0", "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/aws-lambda": "^8.10.72", "@types/jest": "^26.0.19", + "@types/lodash": "^4.14.168", "@types/node": "^14.14.16", "@typescript-eslint/eslint-plugin": "^4.11.1", "@typescript-eslint/parser": "^4.11.1", @@ -19,6 +24,7 @@ "jest": "^26.6.3", "lerna": "^3.22.1", "ts-jest": "^26.4.4", + "ts-node": "^9.1.1", "typescript": "^4.1.3" } }, @@ -1144,7 +1150,6 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -2046,9 +2051,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -2133,9 +2135,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -2352,9 +2351,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -2588,9 +2584,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -2729,9 +2722,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -2794,9 +2784,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3007,9 +2994,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3062,9 +3046,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3227,9 +3208,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3670,9 +3648,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3753,9 +3728,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3888,9 +3860,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3941,9 +3910,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -4397,6 +4363,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.72", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.72.tgz", + "integrity": "sha512-jOrTwAhSiUtBIN/QsWNKlI4+4aDtpZ0sr2BRvKW6XQZdspgHUSHPcuzxbzCRiHUiDQ+0026u5TSE38VyIhNnfA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", @@ -4497,6 +4469,12 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -4937,6 +4915,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -6669,6 +6653,12 @@ "node": ">=10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6945,6 +6935,15 @@ "wrappy": "1" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -7235,8 +7234,7 @@ "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" + "optionator": "^0.8.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -8980,7 +8978,6 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", - "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "bin": { @@ -10159,7 +10156,6 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", @@ -10654,7 +10650,6 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -10961,10 +10956,9 @@ } }, "node_modules/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", @@ -15116,6 +15110,32 @@ "node": ">=10" } }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -16087,6 +16107,15 @@ "node": ">=6" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -19748,6 +19777,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "@types/aws-lambda": { + "version": "8.10.72", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.72.tgz", + "integrity": "sha512-jOrTwAhSiUtBIN/QsWNKlI4+4aDtpZ0sr2BRvKW6XQZdspgHUSHPcuzxbzCRiHUiDQ+0026u5TSE38VyIhNnfA==", + "dev": true + }, "@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", @@ -19848,6 +19883,12 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -20167,6 +20208,12 @@ } } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -21561,6 +21608,12 @@ "yaml": "^1.10.0" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -21779,6 +21832,12 @@ "wrappy": "1" } }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -24950,10 +25009,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -28275,6 +28333,20 @@ } } }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -29055,6 +29127,12 @@ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/packages/logging/package.json b/packages/logger/package.json similarity index 68% rename from packages/logging/package.json rename to packages/logger/package.json index 501dd30faa..ebdcb40c5b 100644 --- a/packages/logging/package.json +++ b/packages/logger/package.json @@ -1,5 +1,5 @@ { - "name": "@aws-lambda-powertools/logging", + "name": "@aws-lambda-powertools/logger", "version": "0.0.0", "description": "The logging package for the AWS Lambda powertools (TypeScript) library", "author": { @@ -17,7 +17,13 @@ "prepublishOnly": "npm test && npm run lint", "preversion": "npm run lint", "version": "npm run format && git add -A src", - "postversion": "git push && git push --tags" + "postversion": "git push && git push --tags", + "example:hello-world": "ts-node examples/hello-world.ts", + "example:hello-world-with-context": "ts-node examples/hello-world-with-context.ts", + "example:custom-logger-options": "ts-node examples/custom-logger-options.ts", + "example:child-logger": "ts-node examples/child-logger.ts", + "example:additional-keys": "ts-node examples/additional-keys.ts", + "example:sample-rate": "ts-node examples/sample-rate.ts" }, "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/master/packages/logging#readme", "license": "MIT", @@ -26,7 +32,9 @@ "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/aws-lambda": "^8.10.72", "@types/jest": "^26.0.19", + "@types/lodash": "^4.14.168", "@types/node": "^14.14.16", "@typescript-eslint/eslint-plugin": "^4.11.1", "@typescript-eslint/parser": "^4.11.1", @@ -34,6 +42,7 @@ "jest": "^26.6.3", "lerna": "^3.22.1", "ts-jest": "^26.4.4", + "ts-node": "^9.1.1", "typescript": "^4.1.3" }, "files": [ @@ -45,5 +54,8 @@ }, "bugs": { "url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues" + }, + "dependencies": { + "lodash": "^4.17.21" } } diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts new file mode 100644 index 0000000000..d963a82fc8 --- /dev/null +++ b/packages/logger/src/Logger.ts @@ -0,0 +1,289 @@ +import { Context } from 'aws-lambda'; +import { LoggerInterface } from '.'; +import { LogItem } from './log'; + +import { cloneDeep, merge } from 'lodash/fp'; +import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; +import { + Environment, + PowertoolAttributes, + LogAttributes, + LoggerOptions, + LogLevel, + LogLevelThresholds, + LambdaFunctionContext, + LoggerInput, + LoggerExtraInput +} from '../types'; +import { LogFormatterInterface, PowertoolLogFormatter } from './formatter'; + +class Logger implements LoggerInterface { + + private static coldStart: boolean = true; + + private customAttributes?: LogAttributes = {}; + + private customConfigService?: ConfigServiceInterface; + + private static readonly defaultLogLevel: LogLevel = 'INFO'; + + private envVarsService?: EnvironmentVariablesService; + + private logFormatter?: LogFormatterInterface; + + private logLevel?: LogLevel; + + private readonly logLevelThresholds: LogLevelThresholds = { + 'DEBUG' : 8, + 'INFO': 12, + 'WARN': 16, + 'ERROR': 20 + }; + + private powertoolAttributes: PowertoolAttributes = {}; + + public constructor(options: LoggerOptions = {}) { + this.setOptions(options); + } + + public addContext(context: Context): void { + if (!this.isContextEnabled()) { + return; + } + + const lambdaContext: Partial = { + arn: context.invokedFunctionArn, + awsRequestId: context.awsRequestId, + memoryLimitInMB: Number(context.memoryLimitInMB), + name: context.functionName, + version: context.functionVersion, + }; + + this.addToPowertoolAttributes({ + lambdaContext + }); + + } + + public createChild(options: LoggerOptions = {}): Logger { + return cloneDeep(this).setOptions(options); + } + + public debug(input: LoggerInput, ...extraInput: LoggerExtraInput): void { + if (!this.shouldPrint('DEBUG')) { + return; + } + this.printLog('DEBUG', this.createLogItem('DEBUG', input, extraInput).getAttributes()); + } + + public error(input: LoggerInput, ...extraInput: LoggerExtraInput): void { + if (!this.shouldPrint('ERROR')) { + return; + } + this.printLog('ERROR', this.createLogItem('ERROR', input, extraInput).getAttributes()); + } + + public info(input: LoggerInput, ...extraInput: LoggerExtraInput): void { + if (!this.shouldPrint('INFO')) { + return; + } + this.printLog('INFO', this.createLogItem('INFO', input, extraInput).getAttributes()); + } + + public static isColdStart(): boolean { + if (Logger.coldStart === true) { + Logger.coldStart = false; + + return true; + } + + return false; + } + + public warn(input: LoggerInput, ...extraInput: LoggerExtraInput): void { + if (!this.shouldPrint('WARN')) { + return; + } + this.printLog('WARN', this.createLogItem('WARN', input, extraInput).getAttributes()); + } + + private addToPowertoolAttributes(...attributesArray: Array>): void { + attributesArray.forEach((attributes: Partial) => { + this.powertoolAttributes = merge(this.getPowertoolAttributes(), attributes); + }); + } + + private createLogItem(logLevel: LogLevel, input: LoggerInput, extraInput: LoggerExtraInput): LogItem { + + const logItem = new LogItem().addAttributes( + this.getLogFormatter().format( + merge({ + logLevel, + timestamp: new Date(), + message: (typeof input === 'string') ? input : input.message + }, + this.getPowertoolAttributes()) + ) + ).addAttributes(this.getCustomAttributes()); + + if (typeof input !== 'string') { + logItem.addAttributes(input); + } + + extraInput.forEach((item: Error | LogAttributes) => { + const attributes = (item instanceof Error) ? { + error: { + name: item.name, + message: item.message, + stack: item.stack, + } + } : item; + logItem.addAttributes(attributes); + }); + + return logItem; + } + + private getCustomAttributes(): LogAttributes { + return this.customAttributes || {}; + } + + private getCustomConfigService(): ConfigServiceInterface | undefined { + return this.customConfigService; + } + + private getEnvVarsService(): EnvironmentVariablesService { + if (!this.envVarsService) { + this.setEnvVarsService(); + } + + return this.envVarsService; + } + + private getLogFormatter(): LogFormatterInterface { + if (!this.logFormatter) { + this.setLogFormatter(); + } + + return this.logFormatter; + } + + private getLogLevel(): LogLevel { + if (this.powertoolAttributes?.logLevel) { + this.setLogLevel(); + } + + return this.logLevel; + } + + private getPowertoolAttributes(): PowertoolAttributes { + return this.powertoolAttributes || {}; + } + + private getSampleRateValue(): number { + if (!this.powertoolAttributes?.sampleRateValue) { + this.setSampleRateValue(); + } + + return this.powertoolAttributes?.sampleRateValue; + } + + private isContextEnabled(): boolean { + return this.getCustomConfigService()?.getIsContextEnabled() === true || this.getEnvVarsService().getIsContextEnabled() === true; + } + + private printLog(logLevel: LogLevel, log: LogAttributes): void { + Object.keys(log).forEach(key => (log[key] === undefined || log[key] === '' || log[key] === null) && delete log[key]); + + console.log(log); + } + + private setCustomAttributes(attributes?: LogAttributes): void { + this.customAttributes = attributes; + } + + private setCustomConfigService(customConfigService?: ConfigServiceInterface): void { + this.customConfigService = customConfigService? customConfigService : undefined; + } + + private setEnvVarsService(): void { + this.envVarsService = new EnvironmentVariablesService(); + } + + private setLogFormatter(logFormatter?: LogFormatterInterface): void { + this.logFormatter = logFormatter || new PowertoolLogFormatter(); + } + + private setLogLevel(logLevel?: LogLevel): void { + this.logLevel = (logLevel || this.getCustomConfigService()?.getLogLevel() || this.getEnvVarsService().getLogLevel() + || Logger.defaultLogLevel) as LogLevel; + } + + private setOptions(options: LoggerOptions = {}): Logger { + const { + logLevel, + serviceName, + sampleRateValue, + logFormatter, + customConfigService, + customAttributes, + environment + } = options; + + this.setEnvVarsService(); + this.setCustomConfigService(customConfigService); + this.setLogLevel(logLevel); + this.setSampleRateValue(sampleRateValue); + this.setLogFormatter(logFormatter); + this.setPowertoolAttributes(serviceName, environment); + this.setCustomAttributes(customAttributes); + + return this; + } + + private setPowertoolAttributes(serviceName?: string, environment?: Environment, customAttributes: LogAttributes = {}): void { + + if (this.isContextEnabled()) { + this.addToPowertoolAttributes( { + lambdaContext: { + coldStart: Logger.isColdStart(), + memoryLimitInMB: this.getEnvVarsService().getFunctionMemory(), + name: this.getEnvVarsService().getFunctionName(), + version:this.getEnvVarsService().getFunctionVersion(), + } + }); + } + + this.addToPowertoolAttributes({ + awsRegion: this.getEnvVarsService().getAwsRegion(), + environment: environment || this.getCustomConfigService()?.getCurrentEnvironment() || this.getEnvVarsService().getCurrentEnvironment(), + sampleRateValue: this.getSampleRateValue(), + serviceName: serviceName || this.getCustomConfigService()?.getServiceName() || this.getEnvVarsService().getServiceName(), + xRayTraceId: this.getEnvVarsService().getXrayTraceId(), + }, customAttributes ); + } + + private setSampleRateValue(sampleRateValue?: number): void { + this.powertoolAttributes.sampleRateValue = sampleRateValue || this.getCustomConfigService()?.getSampleRateValue() + || this.getEnvVarsService().getSampleRateValue(); + } + + private shouldPrint(logLevel: LogLevel): boolean { + if (this.logLevelThresholds[logLevel] >= this.logLevelThresholds[this.getLogLevel()]) { + return true; + } + + // TODO: refactor this logic (Math.random() does not provide cryptographically secure random numbers) + const sampleRateValue = this.getSampleRateValue(); + if (sampleRateValue && (sampleRateValue === 1 || Math.random() < sampleRateValue)) { + return true; + } + + return false; + } + +} + +export { + Logger +}; \ No newline at end of file diff --git a/packages/logger/src/LoggerInterface.ts b/packages/logger/src/LoggerInterface.ts new file mode 100644 index 0000000000..1b0a98919c --- /dev/null +++ b/packages/logger/src/LoggerInterface.ts @@ -0,0 +1,17 @@ +import { LoggerExtraInput, LoggerInput } from '../types'; + +interface LoggerInterface { + + debug(input: LoggerInput, ...extraInput: LoggerExtraInput): void + + error(input: LoggerInput, ...extraInput: LoggerExtraInput): void + + info(input: LoggerInput, ...extraInput: LoggerExtraInput): void + + warn(input: LoggerInput, ...extraInput: LoggerExtraInput): void + +} + +export { + LoggerInterface +}; \ No newline at end of file diff --git a/packages/logger/src/config/ConfigService.ts b/packages/logger/src/config/ConfigService.ts new file mode 100644 index 0000000000..526f7cb892 --- /dev/null +++ b/packages/logger/src/config/ConfigService.ts @@ -0,0 +1,28 @@ +import { ConfigServiceInterface } from '.'; + +abstract class ConfigService implements ConfigServiceInterface { + + // Custom environment variables + protected contextEnabledVariable = 'POWERTOOLS_CONTEXT_ENABLED'; + protected currentEnvironmentVariable = 'ENVIRONMENT'; + protected logLevelVariable = 'LOG_LEVEL'; + protected sampleRateValueVariable = 'POWERTOOLS_LOGGER_SAMPLE_RATE'; + protected serviceNameVariable = 'POWERTOOLS_SERVICE_NAME'; + + abstract get(name: string): string; + + abstract getCurrentEnvironment(): string; + + abstract getIsContextEnabled(): boolean; + + abstract getLogLevel(): string; + + abstract getSampleRateValue(): number | undefined; + + abstract getServiceName(): string; + +} + +export { + ConfigService +}; \ No newline at end of file diff --git a/packages/logger/src/config/ConfigServiceInterface.ts b/packages/logger/src/config/ConfigServiceInterface.ts new file mode 100644 index 0000000000..b04f4866e9 --- /dev/null +++ b/packages/logger/src/config/ConfigServiceInterface.ts @@ -0,0 +1,19 @@ +interface ConfigServiceInterface { + + get(name: string): string + + getCurrentEnvironment(): string + + getIsContextEnabled(): boolean + + getLogLevel(): string + + getSampleRateValue(): number | undefined + + getServiceName(): string + +} + +export { + ConfigServiceInterface +}; \ No newline at end of file diff --git a/packages/logger/src/config/EnvironmentVariablesService.ts b/packages/logger/src/config/EnvironmentVariablesService.ts new file mode 100644 index 0000000000..bc61f23fb7 --- /dev/null +++ b/packages/logger/src/config/EnvironmentVariablesService.ts @@ -0,0 +1,64 @@ +import { ConfigService } from '.'; + +class EnvironmentVariablesService extends ConfigService { + + // Reserved environment variables + private awsRegionVariable = 'AWS_REGION'; + private functionNameVariable = 'AWS_LAMBDA_FUNCTION_NAME'; + private functionVersionVariable = 'AWS_LAMBDA_FUNCTION_VERSION'; + private memoryLimitInMBVariable = 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE'; + private xRayTraceIdVariable = '_X_AMZN_TRACE_ID'; + + public get(name: string): string { + return process.env[name]?.trim() || ''; + } + + public getAwsRegion(): string { + return this.get(this.awsRegionVariable); + } + + public getCurrentEnvironment(): string { + return this.get(this.currentEnvironmentVariable); + } + + public getFunctionMemory(): number { + const value = this.get(this.memoryLimitInMBVariable); + + return Number(value); + } + + public getFunctionName(): string { + return this.get(this.functionNameVariable); + } + + public getFunctionVersion(): string { + return this.get(this.functionVersionVariable); + } + + public getIsContextEnabled(): boolean { + return [ '1', 'TRUE', 'ON' ].includes(this.get(this.contextEnabledVariable).toUpperCase()); + } + + public getLogLevel(): string { + return this.get(this.logLevelVariable); + } + + public getSampleRateValue(): number | undefined { + const value = this.get(this.sampleRateValueVariable); + + return (value && value.length > 0) ? Number(value) : undefined; + } + + public getServiceName(): string { + return this.get(this.serviceNameVariable); + } + + public getXrayTraceId(): string { + return this.get(this.xRayTraceIdVariable); + } + +} + +export { + EnvironmentVariablesService, +}; \ No newline at end of file diff --git a/packages/logger/src/config/index.ts b/packages/logger/src/config/index.ts new file mode 100644 index 0000000000..b8a77f2ab4 --- /dev/null +++ b/packages/logger/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './ConfigService'; +export * from './ConfigServiceInterface'; +export * from './EnvironmentVariablesService'; \ No newline at end of file diff --git a/packages/logger/src/formatter/LogFormatter.ts b/packages/logger/src/formatter/LogFormatter.ts new file mode 100644 index 0000000000..fe062191dd --- /dev/null +++ b/packages/logger/src/formatter/LogFormatter.ts @@ -0,0 +1,15 @@ +import { LogFormatterInterface } from '.'; +import { LogAttributes, UnformattedAttributes } from '../../types'; + +abstract class LogFormatter implements LogFormatterInterface { + + abstract format(attributes: UnformattedAttributes): LogAttributes; + + public formatTimestamp(now: Date): string { + return now.toISOString(); + } +} + +export { + LogFormatter +}; \ No newline at end of file diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts new file mode 100644 index 0000000000..82492fe36c --- /dev/null +++ b/packages/logger/src/formatter/LogFormatterInterface.ts @@ -0,0 +1,11 @@ +import { LogAttributes, UnformattedAttributes } from '../../types'; + +interface LogFormatterInterface { + + format(attributes: UnformattedAttributes): LogAttributes + +} + +export { + LogFormatterInterface +}; \ No newline at end of file diff --git a/packages/logger/src/formatter/PowertoolLogFormatter.ts b/packages/logger/src/formatter/PowertoolLogFormatter.ts new file mode 100644 index 0000000000..da747dfd20 --- /dev/null +++ b/packages/logger/src/formatter/PowertoolLogFormatter.ts @@ -0,0 +1,27 @@ +import { LogFormatter } from '.'; +import { PowertoolLog } from '../../types/formats'; +import { UnformattedAttributes } from '../../types'; + +class PowertoolLogFormatter extends LogFormatter { + + public format(attributes: UnformattedAttributes): PowertoolLog { + return { + aws_request_id: attributes.lambdaContext?.awsRequestId, + cold_start: attributes.lambdaContext?.coldStart, + lambda_function_arn: attributes.lambdaContext?.arn, + lambda_function_memory_size: attributes.lambdaContext?.memoryLimitInMB, + lambda_function_name: attributes.lambdaContext?.name, + level: attributes.logLevel, + message: attributes.message, + sampling_rate: attributes.sampleRateValue, + service: attributes.serviceName, + timestamp: this.formatTimestamp(attributes.timestamp), + xray_trace_id: attributes.xRayTraceId, + }; + } + +} + +export { + PowertoolLogFormatter +}; \ No newline at end of file diff --git a/packages/logger/src/formatter/index.ts b/packages/logger/src/formatter/index.ts new file mode 100644 index 0000000000..4c88132ead --- /dev/null +++ b/packages/logger/src/formatter/index.ts @@ -0,0 +1,3 @@ +export * from './LogFormatter'; +export * from './LogFormatterInterface'; +export * from './PowertoolLogFormatter'; \ No newline at end of file diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000000..24e196c141 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,2 @@ +export * from './Logger'; +export * from './LoggerInterface'; \ No newline at end of file diff --git a/packages/logger/src/log/LogItem.ts b/packages/logger/src/log/LogItem.ts new file mode 100644 index 0000000000..7c94cceaef --- /dev/null +++ b/packages/logger/src/log/LogItem.ts @@ -0,0 +1,27 @@ +import { LogAttributes } from '../../types'; +import { LogItemInterface } from '.'; +import { merge } from 'lodash/fp'; + +class LogItem implements LogItemInterface { + + private attributes: LogAttributes = {}; + + public addAttributes(attributes: LogAttributes): LogItem { + this.attributes = merge(this.attributes, attributes); + + return this; + } + + public getAttributes(): LogAttributes { + return this.attributes; + } + + public toJSON(): string { + return JSON.stringify(this.getAttributes()); + } + +} + +export { + LogItem +}; \ No newline at end of file diff --git a/packages/logger/src/log/LogItemInterface.ts b/packages/logger/src/log/LogItemInterface.ts new file mode 100644 index 0000000000..13aad006a8 --- /dev/null +++ b/packages/logger/src/log/LogItemInterface.ts @@ -0,0 +1,15 @@ +import { LogAttributes } from '../../types/Log'; + +interface LogItemInterface { + + addAttributes(attributes: LogAttributes): void + + getAttributes(): LogAttributes + + toJSON(): string + +} + +export { + LogItemInterface +}; \ No newline at end of file diff --git a/packages/logger/src/log/index.ts b/packages/logger/src/log/index.ts new file mode 100644 index 0000000000..4491038de9 --- /dev/null +++ b/packages/logger/src/log/index.ts @@ -0,0 +1,2 @@ +export * from './LogItem'; +export * from './LogItemInterface'; \ No newline at end of file diff --git a/packages/logger/tests/helpers/index.ts b/packages/logger/tests/helpers/index.ts new file mode 100644 index 0000000000..d3e043d223 --- /dev/null +++ b/packages/logger/tests/helpers/index.ts @@ -0,0 +1 @@ +export * from './populate-environment-variables'; \ No newline at end of file diff --git a/packages/logger/tests/helpers/populate-environment-variables.ts b/packages/logger/tests/helpers/populate-environment-variables.ts new file mode 100644 index 0000000000..45489522a4 --- /dev/null +++ b/packages/logger/tests/helpers/populate-environment-variables.ts @@ -0,0 +1,17 @@ +const populateEnvironmentVariables = (): void => { + + // Reserved variables + process.env._X_AMZN_TRACE_ID = 'abcdef123456abcdef123456abcdef123456'; + process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-lambda-function'; + process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128'; + process.env.AWS_REGION = 'eu-central-1'; + + // Powertools variables + process.env.LOG_LEVEL = 'DEBUG'; + process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; + +}; + +export { + populateEnvironmentVariables +}; \ No newline at end of file diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts new file mode 100644 index 0000000000..5ed5cc910e --- /dev/null +++ b/packages/logger/tests/unit/Logger.test.ts @@ -0,0 +1,129 @@ +import { Logger } from '../../src'; +import { populateEnvironmentVariables } from '../helpers'; + +const mockDate = new Date(1466424490000); +const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as unknown as string); + +const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + +describe('Logger', () => { + + const originalEnvironmentVariables = process.env; + + beforeAll(() => { + populateEnvironmentVariables(); + }); + + beforeEach(() => { + consoleSpy.mockClear(); + dateSpy.mockClear(); + }); + + afterAll(() => { + process.env = originalEnvironmentVariables; + }); + + test('should return a valid INFO log', () => { + + const logger = new Logger(); + + logger.info('foo'); + logger.info('foo', { bar: 'baz' }); + + expect(console.log).toBeCalledTimes(2); + expect(console.log).toHaveBeenNthCalledWith(1, { + message: 'foo', + service: 'hello-world', + level: 'INFO', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + expect(console.log).toHaveBeenNthCalledWith(2, { + bar: 'baz', + message: 'foo', + service: 'hello-world', + level: 'INFO', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + + }); + + test('should return a valid ERROR log', () => { + + const logger = new Logger(); + + logger.error('foo'); + logger.error('foo', { bar: 'baz' }); + + expect(console.log).toBeCalledTimes(2); + expect(console.log).toHaveBeenNthCalledWith(1, { + message: 'foo', + service: 'hello-world', + level: 'ERROR', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + expect(console.log).toHaveBeenNthCalledWith(2, { + bar: 'baz', + message: 'foo', + service: 'hello-world', + level: 'ERROR', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + }); + + test('should return a valid DEBUG log', () => { + + const logger = new Logger(); + + logger.debug('foo'); + logger.debug('foo', { bar: 'baz' }); + + expect(console.log).toBeCalledTimes(2); + expect(console.log).toHaveBeenNthCalledWith(1, { + message: 'foo', + service: 'hello-world', + level: 'DEBUG', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + expect(console.log).toHaveBeenNthCalledWith(2, { + bar: 'baz', + message: 'foo', + service: 'hello-world', + level: 'DEBUG', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + }); + + test('should return a valid WARN log', () => { + + const logger = new Logger(); + + logger.warn('foo'); + logger.warn( { message: 'foo', bar: 'baz' }); + + expect(console.log).toBeCalledTimes(2); + expect(console.log).toHaveBeenNthCalledWith(1, { + timestamp: '2016-06-20T12:08:10.000Z', + message: 'foo', + level: 'WARN', + service: 'hello-world', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + expect(console.log).toHaveBeenNthCalledWith(2, { + bar: 'baz', + level: 'WARN', + message: 'foo', + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: 'abcdef123456abcdef123456abcdef123456' + }); + + }); + +}); + diff --git a/packages/logging/tsconfig.json b/packages/logger/tsconfig.json similarity index 93% rename from packages/logging/tsconfig.json rename to packages/logger/tsconfig.json index 9b62cf735e..64b249fa6c 100644 --- a/packages/logging/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -14,7 +14,7 @@ "baseUrl": "src/", "rootDirs": [ "src/" ] }, - "include": [ "src/**/*" ], + "include": [ "src/**/*", "examples/**/*" ], "exclude": [ "./node_modules", "**/tests/*"], "watchOptions": { "watchFile": "useFsEvents", diff --git a/packages/logger/types/Log.ts b/packages/logger/types/Log.ts new file mode 100644 index 0000000000..fba01ae081 --- /dev/null +++ b/packages/logger/types/Log.ts @@ -0,0 +1,28 @@ +type LogLevelDebug = 'DEBUG'; +type LogLevelInfo = 'INFO'; +type LogLevelWarn = 'WARN'; +type LogLevelError = 'ERROR'; + +type LogLevel = LogLevelDebug | LogLevelInfo | LogLevelWarn | LogLevelError; + +type LogLevelThresholds = { + [key in LogLevel]: number; +}; + +type LogAttributeValue = string | number | boolean | null | undefined | LogAttributeValue[] | { [key: string]: LogAttributeValue } | Error; +type LogAttributes = { [key: string]: LogAttributeValue }; + +type LogAttributesWithMessage = LogAttributes & { + message: string +}; + +type Environment = 'dev' | 'local' | 'staging' | 'prod' | string; + +export { + LogAttributesWithMessage, + LogAttributeValue, + Environment, + LogLevelThresholds, + LogAttributes, + LogLevel +}; \ No newline at end of file diff --git a/packages/logger/types/Logger.ts b/packages/logger/types/Logger.ts new file mode 100644 index 0000000000..e322979f01 --- /dev/null +++ b/packages/logger/types/Logger.ts @@ -0,0 +1,56 @@ +import { ConfigServiceInterface } from '../src/config'; +import { LogFormatterInterface } from '../src/formatter'; +import { Environment, LogAttributes, LogAttributesWithMessage, LogLevel } from './Log'; + +type LoggerOptions = { + logLevel?: LogLevel + serviceName?: string + sampleRateValue?: number + logFormatter?: LogFormatterInterface + customConfigService?: ConfigServiceInterface + customAttributes?: LogAttributes + environment?: Environment +}; + +type LambdaFunctionContext = { + name: string + memoryLimitInMB: number + version: string + coldStart: boolean + arn: string + awsRequestId: string +}; + +type PowertoolAttributes = LogAttributes & { + environment?: Environment + serviceName: string + sampleRateValue?: number + lambdaFunctionContext: LambdaFunctionContext + xRayTraceId?: string + awsRegion: string +}; + +type UnformattedAttributes = PowertoolAttributes & { + environment?: Environment + error?: Error + serviceName: string + sampleRateValue?: number + lambdaContext?: LambdaFunctionContext + xRayTraceId?: string + awsRegion: string + logLevel: LogLevel + timestamp: Date + message: string +}; + +type LoggerInput = string | LogAttributesWithMessage; +type LoggerExtraInput = Array; + +export { + LoggerInput, + LoggerExtraInput, + LambdaFunctionContext, + UnformattedAttributes, + PowertoolAttributes, + LoggerOptions +}; \ No newline at end of file diff --git a/packages/logger/types/formats/PowertoolLog.ts b/packages/logger/types/formats/PowertoolLog.ts new file mode 100644 index 0000000000..9550916f07 --- /dev/null +++ b/packages/logger/types/formats/PowertoolLog.ts @@ -0,0 +1,97 @@ +import { LogAttributes, LogLevel } from '../Log'; + +type PowertoolLog = 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 casted 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 { + PowertoolLog +}; \ No newline at end of file diff --git a/packages/logger/types/formats/index.ts b/packages/logger/types/formats/index.ts new file mode 100644 index 0000000000..e5a9bfb5c5 --- /dev/null +++ b/packages/logger/types/formats/index.ts @@ -0,0 +1 @@ +export * from './PowertoolLog'; \ No newline at end of file diff --git a/packages/logger/types/index.ts b/packages/logger/types/index.ts new file mode 100644 index 0000000000..3b4ef64069 --- /dev/null +++ b/packages/logger/types/index.ts @@ -0,0 +1,2 @@ +export * from './Log'; +export * from './Logger'; \ No newline at end of file diff --git a/packages/logging/README.md b/packages/logging/README.md deleted file mode 100644 index 5ba0912576..0000000000 --- a/packages/logging/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `logging` - -> TODO: description - -## Usage - -``` -import { foo } from "@aws-lambda-powertools/logging" - -// TODO: DEMONSTRATE API -``` diff --git a/packages/logging/src/index.ts b/packages/logging/src/index.ts deleted file mode 100644 index 0ef8de8089..0000000000 --- a/packages/logging/src/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -enum LogLevel { - DEBUG = 1, - INFO = 2, - WARNING = 3, - ERROR = 4, - CRITICAL = 5 -} - -interface LoggerConfig { - LogLevel?: LogLevel -} - -class Logger { - private CURRENT_LOG_LEVEL: LogLevel = LogLevel.INFO; - - public constructor(config?: LoggerConfig) { - config = config || {}; - this.setInitialLogLevel(config); - } - - private setInitialLogLevel(config: LoggerConfig):void { - if (config.LogLevel) { - this.CURRENT_LOG_LEVEL = config.LogLevel; - } - else if (process.env.LOG_LEVEL) { - const environmentVariableLevel = process.env.LOG_LEVEL; - if (environmentVariableLevel in LogLevel) { - this.CURRENT_LOG_LEVEL = LogLevel[environmentVariableLevel as keyof typeof LogLevel]; - } //else { - // this.Warn(`LOG_LEVEL environment value was not valid, Received ${environmentVariableLevel} and expected one of ${Object.keys(LogLevel).join(', ')}`); - // } - } - } - - public getCurrentLogLevel():LogLevel { - return this.CURRENT_LOG_LEVEL; - } -} - -export { - Logger, - LogLevel -}; - diff --git a/packages/logging/tests/unit/logger.test.ts b/packages/logging/tests/unit/logger.test.ts deleted file mode 100644 index 4b1b97b41f..0000000000 --- a/packages/logging/tests/unit/logger.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Logger, LogLevel } from '../../src'; - -describe('Logger Class', () => { - test('Should return a valid instance', () => { - const logger = new Logger(); - expect(typeof logger).toBe('object'); - }); - describe('Instantiation Log levels', () => { - afterEach(() => { - delete process.env.LOG_LEVEL; - }); - test('Should default to INFO if no level passed', () => { - const logger = new Logger(); - expect(logger.getCurrentLogLevel()).toBe(LogLevel.INFO); - }); - test('Log level passed in constructor should be maintained', () => { - const loggerDEBUG = new Logger({ LogLevel: LogLevel.DEBUG }); - expect(loggerDEBUG.getCurrentLogLevel()).toBe(LogLevel.DEBUG); - const loggerWARN = new Logger({ LogLevel: LogLevel.WARNING }); - expect(loggerWARN.getCurrentLogLevel()).toBe(LogLevel.WARNING); - const loggerCRITICAL = new Logger({ LogLevel: 5 }); - expect(loggerCRITICAL.getCurrentLogLevel()).toBe(LogLevel.CRITICAL); - }); - test('Log level passed as ENV var should be set', () => { - process.env.LOG_LEVEL = 'ERROR'; - const logger = new Logger(); - expect(logger.getCurrentLogLevel()).toBe(LogLevel.ERROR); - }); - test('Log level passed in constructor should override environment variable', () => { - process.env.LOG_LEVEL = 'ERROR'; - const logger = new Logger({ LogLevel: LogLevel.CRITICAL }); - expect(logger.getCurrentLogLevel()).toBe(LogLevel.CRITICAL); - process.env.LOG_LEVEL = 'DEBUG'; - const loggerDEBUG = new Logger(); - expect(loggerDEBUG.getCurrentLogLevel()).toBe(LogLevel.DEBUG); - }); - test('Invalid ENV param should throw warning, and set log level to INFO', () => { - process.env.LOG_LEVEL = 'ALERT'; - const logger = new Logger(); - expect(logger.getCurrentLogLevel()).toBe(LogLevel.INFO); - }); - }); -}); - diff --git a/tests/resources/contexts/hello-world.ts b/tests/resources/contexts/hello-world.ts new file mode 100644 index 0000000000..86d5ec18f1 --- /dev/null +++ b/tests/resources/contexts/hello-world.ts @@ -0,0 +1,20 @@ +import { Context } from 'aws-lambda'; + +const context: Context = { + callbackWaitsForEmptyEventLoop: true, + functionVersion: '$LATEST', + functionName: 'foo-bar-function', + memoryLimitInMB: '128', + logGroupName: '/aws/lambda/foo-bar-function-123456abcdef', + logStreamName: '2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456', + invokedFunctionArn: 'arn:aws:lambda:eu-central-1:123456789012:function:Example', + awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', + getRemainingTimeInMillis: () => 1234, + done: () => console.log('Done!'), + fail: () => console.log('Failed!'), + succeed: () => console.log('Succeeded!'), +}; + +export { + context +}; \ No newline at end of file diff --git a/tests/resources/events/custom/hello-world.json b/tests/resources/events/custom/hello-world.json new file mode 100644 index 0000000000..4ccb6ff485 --- /dev/null +++ b/tests/resources/events/custom/hello-world.json @@ -0,0 +1,5 @@ +{ + "key1": "value1", + "key2": "value2", + "key3": "value3" +} \ No newline at end of file