diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f2d185cba..06a780b555 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Please read through this document before submitting any issues or pull requests information to effectively respond to your bug report or contribution. ## Security issue notifications + If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. ## Reporting Bugs/Feature Requests @@ -35,7 +36,7 @@ Contributions via pull requests are much appreciated. Before sending us a pull r To send us a pull request, please follow these steps: 1. Fork the repository. -2. Install dependencies: `npm install` +2. Install dependencies: `npm ci; npm run lerna-ci` 3. Prepare utilities like commit hooks: `npm run init-environment` 4. Create a new branch to focus on the specific change you are contributing e.g. `git checkout -b improv/logger-debug-sampling` 5. Run all tests, and code baseline checks: `npm run test` @@ -54,8 +55,9 @@ You might find useful to run both the documentation website and the API referenc * **Docs website**: You can build and start a local docs website by running these two commands. - - `npm run docs-buildDockerImage` OR `docker build -t squidfunk/mkdocs-material ./docs/` - - `npm run docs-runLocalDocker` OR `docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material` + +* `npm run docs-buildDockerImage` OR `docker build -t squidfunk/mkdocs-material ./docs/` +* `npm run docs-runLocalDocker` OR `docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material` ### Tests @@ -67,12 +69,10 @@ Unit tests, under `tests/unit` folder are standard [Jest](https://jestjs.io) tes End-to-end tests, under `tests/e2e` folder, will test the module features by deploying AWS Lambda functions into your AWS Account. We use [aws-cdk](https://docs.aws.amazon.com/cdk/v1/guide/getting_started.html) v1 library (not v2 due to [this cdk issue](https://github.com/aws/aws-cdk/issues/18211)) for Typescript for creating infrastructure, and [aws-sdk-js v2](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/) for invoking the functions and assert on the expected behaviors. All steps are also executed by Jest. - Running end-to-end tests will deploy AWS resources. You will need an AWS account and the tests might incur costs. The cost from **some services** are covered by the [AWS Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all) but not all of them. If you don't have an AWS Account follow [these instructions to create one](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). When contributing to this repository, these end-to-end tests are run by the maintainers before merging a PR. - **Unit tests** **Write** @@ -89,7 +89,8 @@ As mentioned before, tests are split into groups thanks to [jest-runner-groups]( **Run** -To run unit tests you can either use +To run unit tests you can either use + * npm task `lerna-test:unit` (`npm run lerna-test:unit`) in root folder to run them all * npm task `test:e2e` (`npm run test:unit`) in module folder (for example: `packages/metrics`) to run the module specific one * jest directly `npx jest --group=unit` in module folder to run the module specific one (You can run selective tests by restricting the group to the one you want. For instance `npx jest --group=unit/metrics/class`) @@ -110,15 +111,16 @@ As mentioned in the previous section, tests are split into groups thanks to [jes See `metrics/tests/e2e/decorator.test.ts` as an example. - **Run** -To run e2e tests you can either use -* npm task `lerna-test:e2e` (`npm run lerna-test:e2e`) in root folder +To run e2e tests you can either use + +* npm task `lerna-test:e2e` (`npm run lerna-test:e2e`) in root folder * npm task `test:e2e` (`npm run test:e2e`) in module folder (for example: `packages/metrics`) to run the module specific one * jest directly `npx jest --group=e2e` in module folder. (You can run selective tests by restricting the group to the one you want. For instance `npx jest --group=e2e/metrics/decorator`) Two important env variable can be used: + * `AWS_PROFILE` to use the right AWS credentials * `DISABLE_TEARDOWN` if you don't want your stack to be destroyed at the end of the test (useful in dev mode when iterating over your code). @@ -127,6 +129,7 @@ Example: `DISABLE_TEARDOWN=true AWS_PROFILE=dev-account npx jest --group=e2e/met **Automate** You can run the end-to-end tests automatically on your forked project by following these steps: + 1. Create an IAM role in your target AWS account, with the least amount of privilege. As mentioned above in this page, we are leveraging CDK to deploy and consequently clean-up resources on AWS. Therefore to run those tests through Github actions you will need to grant specific permissions to your workflow. @@ -140,15 +143,15 @@ You can run the end-to-end tests automatically on your forked project by followi More information about: - - [Github OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/ - - ["Configure AWS Credentials" Action For GitHub Actions](https://github.com/aws-actions/configure-aws-credentials/) + * [Github OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/) + * ["Configure AWS Credentials" Action For GitHub Actions](https://github.com/aws-actions/configure-aws-credentials/) + 2. Add your new role into your [Github fork secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) with name `AWS_ROLE_ARN_TO_ASSUME`. 3. In your forked repository, go to the "Actions" tabs, select the `run-e2e-tests` workflow. 4. In the run-e2e-tests workflow page, select "Run workflow" and run it on the desired branch. > :warning: **Don't automatically run end-to-end tests on branch push or PRs**. A malicious attacker can submit a pull request to attack your AWS account. Ideally, use a blank account without any important workload/data, and limit `AWS_ROLE_ARN_TO_ASSUME` permission to least minimum privilege. - ### Examples As part of the repo you will find an examples folder at the root. This folder contains examples (written with CDK for now) of deployable AWS Lambda functions using Powertools. @@ -156,21 +159,22 @@ As part of the repo you will find an examples folder at the root. This folder co To test your updates with these examples you just have to: 1. Build your local version of *aws-lambda-powertools-typescript* npm packages with `npm run lerna-package` -1. Update their references in examples +2. Update their references in examples ``` cd examples/cdk npm install ../../packages/**/dist/aws-lambda-powertools-* ``` -1. Run cdk tests +3. Run cdk tests ``` npm run test ``` -1. Deploy +4. Deploy ``` npm run cdk deploy ``` Previous command will deploy AWS resources therefore you will need an AWS account and it might incur in some costs which should be covered by the [AWS Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all). If you don't have an AWS Account follow [these instructions to create one](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). + ### Conventions Category | Convention @@ -203,4 +207,4 @@ TODO See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. \ No newline at end of file +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/docs/core/logger.md b/docs/core/logger.md index bb7b3dd432..316a526efd 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -10,7 +10,6 @@ description: Core utility **Do not use this library for production workloads.** - Logger provides an opinionated logger with output structured as JSON. ## Key features @@ -27,9 +26,7 @@ Logger provides an opinionated logger with output structured as JSON. Install the library in your project: ```shell - npm install @aws-lambda-powertools/logger - ``` ### Utility settings @@ -43,7 +40,6 @@ Setting | Description **Logging level** | Sets how verbose Logger should be (INFO, by default). Supported values are: `DEBUG`, `INFO`, `WARN`, `ERROR` | `LOG_LEVEL` | `logLevel` **Service name** | Sets the name of service of which the Lambda function is part of, that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `serviceName` - For a **complete list** of supported environment variables, refer to [this section](./../index.md#environment-variables). #### Example using AWS Serverless Application Model (SAM) @@ -51,7 +47,7 @@ For a **complete list** of supported environment variables, refer to [this secti === "handler.ts" ```typescript hl_lines="1 4" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; // Logger parameters fetched from the environment variables (see template.yaml tab) const logger = new Logger(); @@ -108,15 +104,15 @@ Key | Example === "Manual" ```typescript hl_lines="7" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; const logger = new Logger(); - export const handler = async (_event, context) => { + export const handler = async (_event, context): Promise => { logger.addContext(context); - logger.info("This is an INFO log with some context"); + logger.info('This is an INFO log with some context'); }; ``` @@ -130,13 +126,13 @@ Key | Example Learn more about [its usage and lifecycle in the official Middy documentation](https://github.com/middyjs/middy#usage){target="_blank"}. ```typescript hl_lines="1-2 10-11" - import { Logger, injectLambdaContext } from "@aws-lambda-powertools/logger"; + import { Logger, injectLambdaContext } from '@aws-lambda-powertools/logger'; import middy from '@middy/core'; const logger = new Logger(); - const lambdaHandler = async (_event: any, _context: any) => { - logger.info("This is an INFO log with some context"); + const lambdaHandler = async (_event: any, _context: any): Promise => { + logger.info('This is an INFO log with some context'); }; export const handler = middy(lambdaHandler) @@ -146,7 +142,7 @@ Key | Example === "Decorator" ```typescript hl_lines="8" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; import { LambdaInterface } from '@aws-lambda-powertools/commons'; const logger = new Logger(); @@ -155,7 +151,7 @@ Key | Example // Decorate your handler class method @logger.injectLambdaContext() public async handler(_event: any, _context: any): Promise { - logger.info("This is an INFO log with some context"); + logger.info('This is an INFO log with some context'); } } @@ -193,39 +189,39 @@ You can append additional persistent keys and values in the logs generated durin === "handler.ts" ```typescript hl_lines="5-12 16-23" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; // Add persistent log keys via the constructor const logger = new Logger({ persistentLogAttributes: { - aws_account_id: "123456789012", - aws_region: "eu-central-1", + aws_account_id: '123456789012', + aws_region: 'eu-central-1', logger: { - name: "@aws-lambda-powertools/logger", - version: "0.0.1", + name: '@aws-lambda-powertools/logger', + version: '0.0.1', } } }); // OR add persistent log keys to an existing Logger instance with the appendKeys method: // logger.appendKeys({ - // aws_account_id: "123456789012", - // aws_region: "eu-central-1", + // aws_account_id: '123456789012', + // aws_region: 'eu-central-1', // logger: { - // name: "@aws-lambda-powertools/logger", - // version: "0.0.1", + // name: '@aws-lambda-powertools/logger', + // version: '0.0.1', // } // }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { // This info log will print all extra custom attributes added above // Extra attributes: logger object with name and version of the logger library, awsAccountId, awsRegion - logger.info("This is an INFO log"); - logger.info("This is another INFO log"); + logger.info('This is an INFO log'); + logger.info('This is another INFO log'); return { - foo: "bar" + foo: 'bar' }; }; @@ -267,33 +263,32 @@ You can append additional persistent keys and values in the logs generated durin You can append additional keys and values in a single log item passing them as parameters. - === "handler.ts" ```typescript hl_lines="14 18-19" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; const logger = new Logger(); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { const myImportantVariable = { - foo: "bar" + foo: 'bar' }; // Pass additional keys and values in single log items // As second parameter - logger.info("This is a log with an extra variable", { data: myImportantVariable }); + logger.info('This is a log with an extra variable', { data: myImportantVariable }); // You can also pass multiple parameters - logger.info("This is a log with 2 extra variables", + logger.info('This is a log with 2 extra variables', { data: myImportantVariable }, - { correlationIds: { myCustomCorrelationId: "foo-bar-baz" }} + { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } } ); return { - foo: "bar" + foo: 'bar' }; }; @@ -328,24 +323,24 @@ The error will be logged with default key name `error`, but you can also pass yo === "handler.ts" ```typescript hl_lines="11 18" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; const logger = new Logger(); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { try { - throw new Error("Unexpected error #1"); + throw new Error('Unexpected error #1'); } catch (error) { // Log information about the error using the default "error" key - logger.error("This is the first error", error); + logger.error('This is the first error', error); } try { - throw new Error("Unexpected error #2"); + throw new Error('Unexpected error #2'); } catch (error) { // Log information about the error using a custom "myCustomErrorKey" key - logger.error("This is the second error", { myCustomErrorKey: error } ); + logger.error('This is the second error', { myCustomErrorKey: error } ); } }; @@ -392,25 +387,25 @@ This can be useful for example if you want to enable multiple Loggers with diffe === "handler.ts" ```typescript hl_lines="9-11 18-19" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; // With this logger, all the INFO logs will be printed const logger = new Logger({ - logLevel: "INFO" + logLevel: 'INFO' }); // With this logger, only the ERROR logs will be printed - const childLogger = parentLogger.createChild({ - logLevel: "ERROR" + const childLogger = logger.createChild({ + logLevel: 'ERROR' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { - logger.info("This is an INFO log, from the parent logger"); - logger.error("This is an ERROR log, from the parent logger"); + logger.info('This is an INFO log, from the parent logger'); + logger.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"); + childLogger.info('This is an INFO log, from the child logger'); + childLogger.error('This is an ERROR log, from the child logger'); }; ``` @@ -452,8 +447,8 @@ For example, by setting the "sample rate" to `0.5`, roughly 50% of your lambda i !!! tip "When is this useful?" In production, to avoid log data pollution and reduce CloudWatch costs, developers are encouraged to use the logger with `logLevel` equal to `ERROR` or `WARN`. - This means that only errors or warnings will be printed. - + This means that only errors or warnings will be printed. + However, it might still be useful to print all the logs (including debug ones) of a very small percentage of invocations to have a better understanding of the behaviour of your code in production even when there are no errors. Sampling decision happens at the Logger initialization. This means sampling may happen significantly more or less than depending on your traffic patterns, for example a steady low number of invocations and thus few cold starts. @@ -461,25 +456,25 @@ For example, by setting the "sample rate" to `0.5`, roughly 50% of your lambda i === "handler.ts" ```typescript hl_lines="6" - import { Logger } from "@aws-lambda-powertools/logger"; + import { Logger } from '@aws-lambda-powertools/logger'; // Notice the log level set to 'ERROR' const logger = new Logger({ - logLevel: "ERROR", + logLevel: 'ERROR', sampleRateValue: 0.5 }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { // This log item (equal to log level 'ERROR') will be printed to standard output // in all Lambda invocations - logger.error("This is an ERROR log"); + logger.error('This is an ERROR log'); // These log items (below the log level 'ERROR') have ~50% chance // of being printed in a Lambda invocation - logger.debug("This is a DEBUG log that has 50% chance of being printed"); - logger.info("This is an INFO log that has 50% chance of being printed"); - logger.warn("This is a WARN log that has 50% chance of being printed"); + logger.debug('This is a DEBUG log that has 50% chance of being printed'); + logger.info('This is an INFO log that has 50% chance of being printed'); + logger.warn('This is a WARN log that has 50% chance of being printed'); // Optional: refresh sample rate calculation on runtime // logger.refreshSampleRateCalculation(); @@ -594,28 +589,28 @@ You can customize the structure (keys and values) of your log items by passing a === "handler.ts" ```typescript hl_lines="2 5" - import { Logger } from "@aws-lambda-powertools/logger"; - import { MyCompanyLogFormatter } from "./utils/formatters/MyCompanyLogFormatter"; + import { Logger } from '@aws-lambda-powertools/logger'; + import { MyCompanyLogFormatter } from './utils/formatters/MyCompanyLogFormatter'; const logger = new Logger({ logFormatter: new MyCompanyLogFormatter(), - logLevel: "DEBUG", - serviceName: "serverlessAirline", + logLevel: 'DEBUG', + serviceName: 'serverlessAirline', sampleRateValue: 0.5, persistentLogAttributes: { awsAccountId: process.env.AWS_ACCOUNT_ID, logger: { - name: "@aws-lambda-powertools/logger", - version: "0.0.1" + name: '@aws-lambda-powertools/logger', + version: '0.0.1' } }, }); - export const handler = async (event, _context) => { + export const handler = async (event, context): Promise => { logger.addContext(context); - - logger.info("This is an INFO log", { correlationIds: { myCustomCorrelationId: "foo-bar-baz" } }); + + logger.info('This is an INFO log', { correlationIds: { myCustomCorrelationId: 'foo-bar-baz' } }); }; ``` @@ -707,35 +702,35 @@ This is a Jest sample that provides the minimum information necessary for Logger === "handler.test.ts" -```typescript - -const dummyContext = { - callbackWaitsForEmptyEventLoop: true, - functionVersion: '$LATEST', - functionName: 'foo-bar-function', - memoryLimitInMB: '128', - logGroupName: '/aws/lambda/foo-bar-function', - logStreamName: '2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456', - invokedFunctionArn: 'arn:aws:lambda:eu-central-1:123456789012:function:foo-bar-function', - awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678', - getRemainingTimeInMillis: () => 1234, - done: () => console.log('Done!'), - fail: () => console.log('Failed!'), - succeed: () => console.log('Succeeded!'), -}; - -describe('MyUnitTest', () => { - - test('Lambda invoked successfully', async () => { - - const testEvent = { test: 'test' }; - await handler(testEvent, dummyContext); + ```typescript - }); + const dummyContext = { + callbackWaitsForEmptyEventLoop: true, + functionVersion: '$LATEST', + functionName: 'foo-bar-function', + memoryLimitInMB: '128', + logGroupName: '/aws/lambda/foo-bar-function', + logStreamName: '2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456', + invokedFunctionArn: 'arn:aws:lambda:eu-central-1:123456789012:function:foo-bar-function', + awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678', + getRemainingTimeInMillis: () => 1234, + done: () => console.log('Done!'), + fail: () => console.log('Failed!'), + succeed: () => console.log('Succeeded!'), + }; -}); + describe('MyUnitTest', () => { -``` + test('Lambda invoked successfully', async () => { + + const testEvent = { test: 'test' }; + await handler(testEvent, dummyContext); + + }); + + }); + + ``` !!! tip - If you don't want to declare your own dummy Lambda Context, you can use [`ContextExamples.helloworldContext`](https://github.com/awslabs/aws-lambda-powertools-typescript/blob/main/packages/commons/src/tests/resources/contexts/hello-world.ts#L3-L16) from [`@aws-lambda-powertools/commons`](https://www.npmjs.com/package/@aws-lambda-powertools/commons). \ No newline at end of file + If you don't want to declare your own dummy Lambda Context, you can use [`ContextExamples.helloworldContext`](https://github.com/awslabs/aws-lambda-powertools-typescript/blob/main/packages/commons/src/tests/resources/contexts/hello-world.ts#L3-L16) from [`@aws-lambda-powertools/commons`](https://www.npmjs.com/package/@aws-lambda-powertools/commons). diff --git a/docs/core/metrics.md b/docs/core/metrics.md index f574d720dd..14ccd103fc 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -40,9 +40,7 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar Install the library in your project: ```shell - npm install @aws-lambda-powertools/metrics - ``` ### Utility settings @@ -105,10 +103,10 @@ You can create metrics using the `addMetric` method, and you can create dimensio const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); metrics.publishStoredMetrics(); - } + }; ``` === "Metrics with custom dimensions" @@ -118,11 +116,11 @@ You can create metrics using the `addMetric` method, and you can create dimensio const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { metrics.addDimension('environment', 'prod'); metrics.addMetric('successfulBooking', MetricUnits.Count, 1); metrics.publishStoredMetrics(); - } + }; ``` !!! tip "Autocomplete Metric Units" @@ -135,6 +133,7 @@ You can create metrics using the `addMetric` method, and you can create dimensio Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behaviour. ### Adding multi-value metrics + You can call `addMetric()` with the same name multiple times. The values will be grouped together in an array. === "addMetric() with the same name" @@ -143,14 +142,13 @@ You can call `addMetric()` with the same name multiple times. The values will be import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; import { Context } from 'aws-lambda'; + const metrics = new Metrics({ namespace:'serverlessAirline', serviceName:'orders' }); - const metrics = new Metrics({namespace:"serverlessAirline", serviceName:"orders"}); - - export const handler = async (event: any, context: Context) => { + export const handler = async (event: any, context: Context): Promise => { metrics.addMetric('performedActionA', MetricUnits.Count, 2); // do something else... metrics.addMetric('performedActionA', MetricUnits.Count, 1); - } + }; ``` === "Example CloudWatch Logs excerpt" @@ -182,6 +180,7 @@ You can call `addMetric()` with the same name multiple times. The values will be "service": "orders" } ``` + ### Adding default dimensions You can add default dimensions to your metrics by passing them as parameters in 4 ways: @@ -202,9 +201,9 @@ You can add default dimensions to your metrics by passing them as parameters in defaultDimensions: { 'environment': 'prod', 'foo': 'bar' } }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; ``` === "Middy middleware" @@ -221,14 +220,14 @@ You can add default dimensions to your metrics by passing them as parameters in const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; // Wrap the handler with middy export const handler = middy(lambdaHandler) // Use the middleware by passing the Metrics instance as a parameter - .use(logMetrics(metrics, { defaultDimensions:{ 'environment': 'prod', 'foo': 'bar' } })); + .use(logMetrics(metrics, { defaultDimensions:{ 'environment': 'prod', 'foo': 'bar' } })); ``` === "setDefaultDimensions method" @@ -239,9 +238,9 @@ You can add default dimensions to your metrics by passing them as parameters in const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); metrics.setDefaultDimensions({ 'environment': 'prod', 'foo': 'bar' }); - export const handler = async (event: any, _context: any) => { + export const handler = async (event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; ``` === "with logMetrics decorator" @@ -255,8 +254,8 @@ You can add default dimensions to your metrics by passing them as parameters in export class MyFunction implements LambdaInterface { // Decorate your handler class method - @metrics.logMetrics({defaultDimensions: DEFAULT_DIMENSIONS}) - public handler(_event: any, _context: any): Promise { + @metrics.logMetrics({ defaultDimensions: DEFAULT_DIMENSIONS }) + public async handler(_event: any, _context: any): Promise { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } } @@ -277,7 +276,6 @@ You can flush metrics automatically using one of the following methods: Using the Middy middleware or decorator will **automatically validate, serialize, and flush** all your metrics. During metrics validation, if no metrics are provided then a warning will be logged, but no exception will be thrown. If you do not use the middleware or decorator, you have to flush your metrics manually. - !!! warning "Metric validation" If metrics are provided, and any of the following criteria are not met, a **`RangeError`** exception will be thrown: @@ -299,7 +297,7 @@ You can manually flush the metrics with `publishStoredMetrics` as follows: const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 10); metrics.publishStoredMetrics(); }; @@ -345,9 +343,9 @@ See below an example of how to automatically flush metrics with the Middy-compat const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; export const handler = middy(lambdaHandler) .use(logMetrics(metrics)); @@ -387,7 +385,6 @@ See below an example of how to automatically flush metrics with the Middy-compat The `logMetrics` decorator of the metrics utility can be used when your Lambda handler function is implemented as method of a Class. - === "handler.ts" ```typescript hl_lines="8" @@ -435,19 +432,21 @@ The `logMetrics` decorator of the metrics utility can be used when your Lambda h If you want to ensure that at least one metric is emitted before you flush them, you can use the `throwOnEmptyMetrics` parameter and pass it to the middleware or decorator: -```typescript hl_lines="11" +=== "handler.ts" + + ```typescript hl_lines="11" import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; import middy from '@middy/core'; const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; export const handler = middy(lambdaHandler) .use(logMetrics(metrics, { throwOnEmptyMetrics: true })); -``` + ``` ### Capturing a cold start invocation as metric @@ -461,12 +460,12 @@ You can optionally capture cold start metrics with the `logMetrics` middleware o const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); - } + }; export const handler = middy(lambdaHandler) - .use(logMetrics(metrics, { captureColdStartMetric: true } })); + .use(logMetrics(metrics, { captureColdStartMetric: true })); ``` === "logMetrics decorator" @@ -475,7 +474,7 @@ You can optionally capture cold start metrics with the `logMetrics` middleware o import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; import { LambdaInterface } from '@aws-lambda-powertools/commons'; - const metrics = new Metrics({namespace: 'serverlessAirline', serviceName: 'orders' }); + const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); export class MyFunction implements LambdaInterface { @@ -512,10 +511,10 @@ You can add high-cardinality data as part of your Metrics log with the `addMetad const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); metrics.addMetadata('bookingId', '7051cd10-6283-11ec-90d6-0242ac120003'); - } + }; export const handler = middy(lambdaHandler) .use(logMetrics(metrics)); @@ -567,7 +566,6 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr **unique metric = (metric_name + dimension_name + dimension_value)** - === "Middy Middleware" ```typescript hl_lines="11 13-14" @@ -576,7 +574,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { metrics.addDimension('metricUnit', 'milliseconds'); // This metric will have the "metricUnit" dimension, and no "metricType" dimension: metrics.addMetric('latency', MetricUnits.Milliseconds, 56); @@ -585,7 +583,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr // This metric will have the "metricType" dimension, and no "metricUnit" dimension: singleMetric.addDimension('metricType', 'business'); singleMetric.addMetric('orderSubmitted', MetricUnits.Count, 1); - } + }; export const handler = middy(lambdaHandler) .use(logMetrics(metrics)); diff --git a/docs/core/tracer.md b/docs/core/tracer.md index 84cdacb652..7cb4b1a983 100644 --- a/docs/core/tracer.md +++ b/docs/core/tracer.md @@ -28,9 +28,7 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray SDK for Node.js](https://gi Install the library in your project: ```shell - npm install @aws-lambda-powertools/tracer - ``` ### Utility settings @@ -53,7 +51,7 @@ For a **complete list** of supported environment variables, refer to [this secti === "handler.ts" ```typescript hl_lines="1 4" - import { Tracer } from "@aws-lambda-powertools/tracer"; + import { Tracer } from '@aws-lambda-powertools/tracer'; // Tracer parameter fetched from the environment variables (see template.yaml tab) const tracer = new Tracer(); @@ -89,7 +87,7 @@ You can quickly start by importing the `Tracer` class, initialize it outside the const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - export const handler = async (_event: any, context: any) => { + export const handler = async (_event: any, context: any): Promise => { const segment = tracer.getSegment(); // This is the facade segment (the one that is created by AWS Lambda) // Create subsegment for the function & set it as active const subsegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); @@ -116,7 +114,7 @@ You can quickly start by importing the `Tracer` class, initialize it outside the } return res; - } + }; ``` === "Middy Middleware" @@ -133,9 +131,9 @@ You can quickly start by importing the `Tracer` class, initialize it outside the const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - const lambdaHandler = async (_event: any, _context: any) => { + const lambdaHandler = async (_event: any, _context: any): Promise => { /* ... */ - } + }; // Wrap the handler with middy export const handler = middy(lambdaHandler) @@ -181,16 +179,16 @@ When using the `captureLambdaHandler` decorator or middleware, Tracer performs t **Metadata** are key-values also associated with traces but not indexed by AWS X-Ray. You can use them to add additional context for an operation using any native object. === "Annotations" - You can add annotations using `putAnnotation` method. + You can add annotations using `putAnnotation` method. ```typescript hl_lines="6" import { Tracer } from '@aws-lambda-powertools/tracer'; const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { tracer.putAnnotation('successfulBooking', true); - } + }; ``` === "Metadata" You can add metadata using `putMetadata` method. @@ -200,10 +198,10 @@ When using the `captureLambdaHandler` decorator or middleware, Tracer performs t const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - export const handler = async (_event: any, _context: any) => { + export const handler = async (_event: any, _context: any): Promise => { const res; /* ... */ tracer.putMetadata('paymentResponse', res); - } + }; ``` ### Methods @@ -217,7 +215,7 @@ You can trace other Class methods using the `captureMethod` decorator or any arb const tracer = new Tracer({ serviceName: 'serverlessAirline' }); - const chargeId = async () => { + const getChargeId = async (): Promise => { const parentSubsegment = tracer.getSegment(); // This is the subsegment currently active // Create subsegment for the function & set it as active const subsegment = parentSubsegment.addNewSubsegment(`### chargeId`); @@ -240,13 +238,13 @@ You can trace other Class methods using the `captureMethod` decorator or any arb tracer.setSegment(parentSubsegment); return res; - } + }; - export const handler = async (_event: any, _context: any) => { - const chargeId = this.getChargeId(); + export const handler = async (_event: any, _context: any): Promise => { + const chargeId = getChargeId(); const payment = collectPayment(chargeId); /* ... */ - } + }; ``` === "Decorator" @@ -286,7 +284,7 @@ You can patch any AWS SDK clients by calling the `captureAWSv3Client` method: === "index.ts" ```typescript hl_lines="5" - import { S3Client } from "@aws-sdk/client-s3"; + import { S3Client } from '@aws-sdk/client-s3'; import { Tracer } from '@aws-lambda-powertools/tracer'; const tracer = new Tracer({ serviceName: 'serverlessAirline' }); @@ -363,6 +361,6 @@ Tracer is disabled by default when not running in the AWS Lambda environment - T ## Tips -- Use annotations on key operations to slice and dice traces, create unique views, and create metrics from it via Trace Groups -- Use a namespace when adding metadata to group data more easily -- Annotations and metadata are added to the current subsegment opened. If you want them in a specific subsegment, [create one](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html#xray-sdk-nodejs-subsegments-lambda) via the escape hatch mechanism +* Use annotations on key operations to slice and dice traces, create unique views, and create metrics from it via Trace Groups +* Use a namespace when adding metadata to group data more easily +* Annotations and metadata are added to the current subsegment opened. If you want them in a specific subsegment, [create one](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html#xray-sdk-nodejs-subsegments-lambda) via the escape hatch mechanism diff --git a/docs/index.md b/docs/index.md index de6ae8626b..9b45add122 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,9 +10,6 @@ description: AWS Lambda Powertools TypeScript **Do not use this library for production workloads.** - - - AWS Lambda Powertools TypeScript provides a suite of utilities for AWS Lambda functions running on the Node.js runtime, to ease the adoption of best practices such as tracing, structured logging, custom metrics, and more. ## Tenets diff --git a/examples/cdk/src/example-function.Tracer.CaptureErrorDisabled.ts b/examples/cdk/src/example-function.Tracer.CaptureErrorDisabled.ts index 6bdea71da0..5f0d42d333 100644 --- a/examples/cdk/src/example-function.Tracer.CaptureErrorDisabled.ts +++ b/examples/cdk/src/example-function.Tracer.CaptureErrorDisabled.ts @@ -7,7 +7,7 @@ import { captureLambdaHandler, Tracer } from '@aws-lambda-powertools/tracer'; process.env.POWERTOOLS_TRACER_ERROR_RESPONSE = 'false'; const tracer = new Tracer({ serviceName: 'tracerCaptureErrorDisabledFn' }); -// In this example we are using the Middy middleware pattern but you can instrument your functions also with the captureLambdaHandler decorator & manual instrumentation +// In this example we are using the Middy middleware pattern, but you can instrument your functions also with the captureLambdaHandler decorator & manual instrumentation export const handler = middy(async (event: typeof Events.Custom.CustomEvent, context: Context) => { tracer.putAnnotation('awsRequestId', context.awsRequestId); tracer.putMetadata('eventPayload', event); diff --git a/examples/cdk/src/example-function.Tracer.CaptureResponseDisabled.ts b/examples/cdk/src/example-function.Tracer.CaptureResponseDisabled.ts index 9dab5ffb1e..4f978cbdb4 100644 --- a/examples/cdk/src/example-function.Tracer.CaptureResponseDisabled.ts +++ b/examples/cdk/src/example-function.Tracer.CaptureResponseDisabled.ts @@ -7,7 +7,7 @@ import { captureLambdaHandler, Tracer } from '@aws-lambda-powertools/tracer'; process.env.POWERTOOLS_TRACER_CAPTURE_RESPONSE = 'false'; const tracer = new Tracer({ serviceName: 'tracerCaptureResponseDisabledFn' }); -// In this example we are using the middleware pattern but you could use also the captureLambdaHandler decorator +// In this example we are using the middleware pattern, but you could use also the captureLambdaHandler decorator export const handler = middy(async (event: typeof Events.Custom.CustomEvent, context: Context) => { tracer.putAnnotation('awsRequestId', context.awsRequestId); tracer.putMetadata('eventPayload', event); diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index d88304b139..9fa63fb5b4 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -248,7 +248,7 @@ class Logger implements ClassThatLogs { * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value * @private */ - private removeCircularDependencies(): (key: string, value: LogAttributes) => void { + private removeCircularDependencies(): (key: string, value: LogAttributes | Error) => void { const references = new WeakSet(); return (key, value) => { diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 764d840b10..733ebcf80d 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -43,22 +43,22 @@ const DEFAULT_NAMESPACE = 'default_namespace'; * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * import { Callback, Context } from 'aws-lambda'; * - * const metrics = new Metrics({namespace:"MyService", serviceName:"withDecorator"}); + * const metrics = new Metrics({ namespace:'MyService', serviceName:'withDecorator' }); * * export class MyFunctionWithDecorator { * * // FYI: Decorator might not render properly in VSCode mouse over due to https://github.com/microsoft/TypeScript/issues/39371 and might show as *@metrics* instead of `@metrics.logMetrics` * - * @metrics.logMetrics({captureColdStartMetric: true, throwOnEmptyMetrics: true, }) - * public handler(_event: any, _context: Context, _callback: Callback): void | Promise { + * @metrics.logMetrics({ captureColdStartMetric: true, throwOnEmptyMetrics: true }) + * public handler(_event: any, _context: Context, _callback: Callback): void | Promise { * // ... * metrics.addMetric('test-metric', MetricUnits.Count, 10); * // ... * } * } * - * export const handlerClass = new MyFunctionWithDecorator() - * export const handler = handlerClass.handler + * export const handlerClass = new MyFunctionWithDecorator(); + * export const handler = handlerClass.handler; * ``` * * ### Standard function @@ -70,10 +70,10 @@ const DEFAULT_NAMESPACE = 'default_namespace'; * ```typescript * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({namespace: "MyService", serviceName: "MyFunction"}); + * const metrics = new Metrics({ namespace: 'MyService', serviceName: 'MyFunction' }); * - * export const handler = async (_event: any, _context: any) => { - * metrics.captureColdStart(); + * export const handler = async (_event: any, _context: any): Promise => { + * metrics.captureColdStartMetric(); * metrics.addMetric('test-metric', MetricUnits.Count, 10); * metrics.publishStoredMetrics(); * }; @@ -161,14 +161,14 @@ class Metrics implements MetricsInterface { * @example * * ```typescript - * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + * import { Metrics } from '@aws-lambda-powertools/metrics'; * import { Context } from 'aws-lambda'; * - * const metrics = new Metrics({namespace:"serverlessAirline", serviceName:"orders"}); + * const metrics = new Metrics({ namespace:'serverlessAirline', serviceName:'orders' }); * - * export const handler = async (event: any, context: Context) => { + * export const handler = async (event: any, context: Context): Promise => { * metrics.captureColdStartMetric(); - * } + * }; * ``` */ public captureColdStartMetric(): void { @@ -207,21 +207,21 @@ class Metrics implements MetricsInterface { * @example * * ```typescript - * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + * import { Metrics } from '@aws-lambda-powertools/metrics'; * import { Callback, Context } from 'aws-lambda'; * - * const metrics = new Metrics({namespace:"CdkExample", serviceName:"withDecorator"}); + * const metrics = new Metrics({ namespace:'CdkExample', serviceName:'withDecorator' }); * * export class MyFunctionWithDecorator { * - * @metrics.logMetrics({captureColdStartMetric: true}) + * @metrics.logMetrics({ captureColdStartMetric: true }) * public handler(_event: any, _context: Context, _callback: Callback): void | Promise { * // ... * } * } * - * export const handlerClass = new MyFunctionWithDecorator() - * export const handler = handlerClass.handler + * export const handlerClass = new MyFunctionWithDecorator(); + * export const handler = handlerClass.handler; * ``` * * @decorator Class @@ -267,9 +267,9 @@ class Metrics implements MetricsInterface { * ```typescript * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({namespace: "CdkExample", serviceName: "MyFunction"}); // Sets metric namespace, and service as a metric dimension + * const metrics = new Metrics({ namespace: 'CdkExample', serviceName: 'MyFunction' }); // Sets metric namespace, and service as a metric dimension * - * export const handler = async (_event: any, _context: any) => { + * export const handler = async (_event: any, _context: any): Promise => { * metrics.addMetric('test-metric', MetricUnits.Count, 10); * metrics.publishStoredMetrics(); * }; @@ -371,15 +371,15 @@ class Metrics implements MetricsInterface { * @example * * ```typescript - * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + * import { Metrics } from '@aws-lambda-powertools/metrics'; * import { Context } from 'aws-lambda'; * - * const metrics = new Metrics({namespace:"serverlessAirline", serviceName:"orders"}); + * const metrics = new Metrics({ namespace:'serverlessAirline', serviceName:'orders' }); * - * export const handler = async (event: any, context: Context) => { + * export const handler = async (event: any, context: Context): Promise => { * metrics.throwOnEmptyMetrics(); * metrics.publishStoredMetrics(); // will throw since no metrics added. - * } + * }; * ``` */ public throwOnEmptyMetrics(): void { diff --git a/packages/tracing/src/Tracer.ts b/packages/tracing/src/Tracer.ts index 2e60ef695f..12e9669aed 100644 --- a/packages/tracing/src/Tracer.ts +++ b/packages/tracing/src/Tracer.ts @@ -130,7 +130,7 @@ class Tracer implements TracerInterface { public constructor(options: TracerOptions = {}) { this.setOptions(options); this.provider = new ProviderService(); - if (this.isTracingEnabled() === false) { + if (!this.isTracingEnabled()) { // Tell x-ray-sdk to not throw an error if context is missing but tracing is disabled this.provider.setContextMissingStrategy(() => ({})); } @@ -462,7 +462,7 @@ class Tracer implements TracerInterface { * @returns segment - The active segment or subsegment in the current scope. */ public getSegment(): Segment | Subsegment { - if (this.isTracingEnabled() === false) { + if (!this.isTracingEnabled()) { return new Subsegment('## Dummy segment'); } const segment = this.provider.getSegment(); @@ -574,7 +574,7 @@ class Tracer implements TracerInterface { * @param segment - Subsegment to set as the current segment */ public setSegment(segment: Segment | Subsegment): void { - if (this.isTracingEnabled() === false) return; + if (!this.isTracingEnabled()) return; return this.provider.setSegment(segment); } diff --git a/typedoc.js b/typedoc.js index d8ee50b296..de645c6431 100644 --- a/typedoc.js +++ b/typedoc.js @@ -1,8 +1,8 @@ module.exports = { out: 'api', - exclude: ['**/node_modules/**', '**/*.test.ts', '**/*.json'], + exclude: [ '**/node_modules/**', '**/*.test.ts', '**/*.json' ], name: 'aws-lambda-powertools-typescript', excludePrivate: true, entryPointStrategy: 'packages', - readme: "./README.md", + readme: './README.md', }; \ No newline at end of file