diff --git a/docs/core/metrics.md b/docs/core/metrics.md index ea69419f1e..89b64b0326 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -32,14 +32,26 @@ Metric has two global settings that will be used across all metrics emitted: Setting | Description | Environment variable | Constructor parameter ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- -**Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` +**Metric namespace** | Logical container where all metrics will be placed e.g. `serverlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` !!! tip "Use your application or main service as the metric namespace to easily group all metrics" > Example using AWS Serverless Application Model (SAM) -=== "template.yml" +=== "index.ts" + + ```typescript hl_lines="5 7" + import { Metrics } from '@aws-lambda-powertools/metrics'; + + + // Sets metric namespace and service via env var + const metrics = new Metrics(); + // OR Sets metric namespace, and service as a metrics parameters + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); + ``` + +=== "sam-template.yml" ```yaml hl_lines="9 10" Resources: @@ -50,22 +62,9 @@ Setting | Description | Environment variable | Constructor parameter Environment: Variables: POWERTOOLS_SERVICE_NAME: payment - POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline + POWERTOOLS_METRICS_NAMESPACE: serverlessAirline ``` -[//]:# (START EDITING FROM HERE DOWN) - -=== "index.ts" - - ```typescript hl_lines="5 7" - import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - - - // Sets metric namespace and service via env var - const metrics = new Metrics(); - // OR Sets metric namespace, and service as a metrics parameters - const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); - ``` You can initialize Metrics anywhere in your code - It'll keep track of your aggregate metrics in memory. @@ -80,10 +79,10 @@ You can create metrics using `addMetric`, and you can create dimensions for all import { Context } from 'aws-lambda'; - const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); export const handler = async (event: any, context: Context) => { - metrics.addMetric('SuccessfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } ``` === "Metrics with custom dimensions" @@ -93,11 +92,11 @@ You can create metrics using `addMetric`, and you can create dimensions for all import { Context } from 'aws-lambda'; - const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); export const handler = async (event: any, context: Context) => { metrics.addDimension('environment', 'prod'); - metrics.addMetric('SuccessfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } ``` @@ -112,9 +111,49 @@ You can create metrics using `addMetric`, and you can create dimensions for all ### Adding default dimensions -You can use `setDefaultDimensions` method to persist dimensions across Lambda invocations. +You can use add default dimensions to your metrics by passing them as parameters in 4 ways: + +* in the constructor +* in the Middy middleware +* using the `setDefaultDimensions` method +* in the decorator + +If you'd like to remove them at some point, you can use `clearDefaultDimensions` method. +See examples below: + +=== "constructor" + + ```typescript hl_lines="7" + import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + + const metrics = new Metrics({ + namespace:"serverlessAirline", + service:"orders", + defaultDimensions: { 'environment': 'prod', 'anotherDimension': 'whatever' } + }); + + export const handler = async (event: any, context: Context) => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + } + ``` + +=== "Middy middleware" + + ```typescript hl_lines="5" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; -If you'd like to remove them at some point, you can use `clearDefaultDimensions` method. + const metrics = new Metrics({ namespace: 'serverlessAirline', service: 'orders' }); + + const lambdaHandler = async (event: any, context: Context) => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + } + + export const handler = middy(lambdaHandler) + .use(logMetrics(metrics, { defaultDimensions:{ 'environment': 'prod', 'anotherDimension': 'whatever' } })); + ``` === "setDefaultDimensions method" @@ -122,57 +161,122 @@ If you'd like to remove them at some point, you can use `clearDefaultDimensions` import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; import { Context } from 'aws-lambda'; - const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); metrics.setDefaultDimensions({ 'environment': 'prod', 'anotherDimension': 'whatever' }); export const handler = async (event: any, context: Context) => { - metrics.addMetric('SuccessfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } ``` === "with logMetrics decorator" - ```typescript hl_lines="6 11" + ```typescript hl_lines="9" import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - import { Context, Callback } from 'aws-lambda'; + import { Context, Callback } from 'aws-lambda'; - - const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); const DEFAULT_DIMENSIONS = {"environment": "prod", "another": "one"}; - export class MyFunction { - @metrics.logMetrics({defaultDimensions: DEFAULT_DIMENSIONS}) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetric('SuccessfulBooking', MetricUnits.Count, 1); + @metrics.logMetrics({defaultDimensions: DEFAULT_DIMENSIONS}) + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + } } ``` ### Flushing metrics -As you finish adding all your metrics, you need to serialize and flush them to standard output. +As you finish adding all your metrics, you need to serialize and "flush them" (= print them to standard output). -#### Using Decorator +You can flush metrics automatically using one of the following methods: - You can do that automatically with the `logMetrics` decorator. +* [Middy-compatible](https://github.com/middyjs/middy){target=_blank} middleware +* class decorator +* manually -!!! warning - Decorators can only be attached to a class declaration, method, accessor, property, or parameter. Therefore, if you are more into standard function, check the next section instead. See the [official doc](https://www.typescriptlang.org/docs/handbook/decorators.html) for more details. +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 raised. +If you do not 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 raised: + + * Maximum of 9 dimensions + * Namespace is set only once (or none) + * Metric units must be [supported by CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html) + + +#### Using Middy middleware + +See below an example of how to automatically flush metrics with the Middy-compatible `logMetrics` middleware. + + +```typescript hl_lines="3 8 11-12" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; + + const metrics = new Metrics({ namespace: 'exampleApplication' , service: 'exampleService' }); + + const lambdaHandler = async (event: any, context: Context) => { + metrics.addMetric('bookingConfirmation', MetricUnits.Count, 1); + } + + export const handler = middy(lambdaHandler) + .use(logMetrics(metrics)); +``` + +=== "Example CloudWatch Logs excerpt" + + ```json hl_lines="2 7 10 15 22" + { + "bookingConfirmation": 1.0, + "_aws": { + "Timestamp": 1592234975665, + "CloudWatchMetrics": [ + { + "Namespace": "exampleApplication", + "Dimensions": [ + [ + "service" + ] + ], + "Metrics": [ + { + "Name": "bookingConfirmation", + "Unit": "Count" + } + ] + } + ] + }, + "service": "exampleService" + } + ``` + +#### Using the class decorator + +!!! info + Decorators can only be attached to a class declaration, method, accessor, property, or parameter. Therefore, if you prefer to write your handler as a standard function rather than a Class method, check the [middleware](#using-a-middleware) or [manual](#manually) method sections instead. + See the [official TypeScript documentation](https://www.typescriptlang.org/docs/handbook/decorators.html) for more details. + +The `logMetrics` decorator of the metrics utility can be used when your Lambda handler function is implemented as method of a Class. -This decorator also **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, if no metrics are provided then a warning will be logged, but no exception will be raised. ```typescript hl_lines="8" import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; import { Context, Callback } from 'aws-lambda'; -const metrics = new Metrics({namespace:"ExampleApplication", service:"ExampleService"}); +const metrics = new Metrics({namespace:"exampleApplication", service:"exampleService"}); export class MyFunction { @metrics.logMetrics() public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetric('BookingConfirmation', MetricUnits.Count, 1); + metrics.addMetric('bookingConfirmation', MetricUnits.Count, 1); } } ``` @@ -181,12 +285,12 @@ export class MyFunction { ```json hl_lines="2 7 10 15 22" { - "BookingConfirmation": 1.0, + "bookingConfirmation": 1.0, "_aws": { "Timestamp": 1592234975665, "CloudWatchMetrics": [ { - "Namespace": "ExampleApplication", + "Namespace": "exampleApplication", "Dimensions": [ [ "service" @@ -194,27 +298,21 @@ export class MyFunction { ], "Metrics": [ { - "Name": "BookingConfirmation", + "Name": "bookingConfirmation", "Unit": "Count" } ] } ] }, - "service": "ExampleService" + "service": "exampleService" } ``` -!!! tip "Metric validation" - If metrics are provided, and any of the following criteria are not met, **`SchemaValidationError`** exception will be raised: - - * Maximum of 9 dimensions - * Namespace is set, and no more than one - * Metric units must be [supported by CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html) #### Manually -If you prefer not to use `logMetrics` decorator because you might want to encapsulate additional logic or avoid having to go for classes encapsulation when doing so, you can manually flush with `purgeStoredMetrics` and clear metrics as follows: +You can manually flush the metrics with `purgeStoredMetrics` as follows: !!! warning Metrics, dimensions and namespace validation still applies. @@ -232,48 +330,72 @@ const lambdaHandler: Handler = async () => { }; ``` -#### Raising SchemaValidationError on empty metrics +#### Throwing a RangeError when no metrics are emitted -If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics` to the **logMetrics** decorator: +If you want to ensure that at least one metric is emitted before you flush them, you can use the `raiseOnEmptyMetrics` parameter and pass it to the middleware or decorator: -```typescript hl_lines="7" -import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - -const metrics = new Metrics(); +```typescript hl_lines="12" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; -class Lambda implements LambdaInterface { + const metrics = new Metrics({namespace:"exampleApplication", service:"exampleService"}); - @metrics.logMetrics({raiseOnEmptyMetrics: true}) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - // This will throw an error unless at least one metric is added + const lambdaHandler = async (event: any, context: Context) => { + metrics.addMetric('bookingConfirmation', MetricUnits.Count, 1); } -} + + export const handler = middy(lambdaHandler) + .use(logMetrics(metrics, { raiseOnEmptyMetrics: true })); ``` -### Capturing cold start metric +### Capturing a cold start invocation as metric -You can optionally capture cold start metrics with `logMetrics` decorator via `captureColdStartMetric` param. +You can optionally capture cold start metrics with the `logMetrics` middleware or decorator via the `captureColdStartMetric` param. -```typescript hl_lines="7" -import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; +=== "logMetrics middleware" -const metrics = new Metrics(); + ```typescript hl_lines="12" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; -class Lambda implements LambdaInterface { + const metrics = new Metrics({namespace: 'serverlessAirline', service: 'orders' }); - @metrics.logMetrics({ captureColdStartMetric: true }) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - ... -``` + const lambdaHandler = async (event: any, context: Context) => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + } + + export const handler = middy(lambdaHandler) + .use(logMetrics(metrics, { captureColdStartMetric: true } })); + ``` + +=== "logMetrics decorator" + + ```typescript hl_lines="9" + import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + import { Context, Callback } from 'aws-lambda'; + import middy from '@middy/core'; + + const metrics = new Metrics({namespace: 'serverlessAirline', service: 'orders' }); + + export class MyFunction { + + @metrics.logMetrics({ captureColdStartMetric: true }) + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + } + } + ``` If it's a cold start invocation, this feature will: * Create a separate EMF blob solely containing a metric named `ColdStart` -* Add `function_name` and `service` dimensions +* Add `function_name`, `service` and default dimensions This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions. -!!! info "We do not emit 0 as a value for ColdStart metric for cost reasons. [Let us know](https://github.com/awslabs/aws-lambda-powertools-typecsript/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=) if you'd prefer a flag to override it" +!!! info "We do not emit 0 as a value for the ColdStart metric for cost-efficiency reasons. [Let us know](https://github.com/awslabs/aws-lambda-powertools-typescript/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=) if you'd prefer a flag to override it." ## Advanced @@ -281,34 +403,34 @@ This has the advantage of keeping cold start metric separate from your applicati You can add high-cardinality data as part of your Metrics log with `addMetadata` method. This is useful when you want to search highly contextual information along with your metrics in your logs. -!!! info +!!! warning **This will not be available during metrics visualization** - Use **dimensions** for this purpose -```typescript hl_lines="9" -import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - -const metrics = new Metrics(); +```typescript hl_lines="8" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; -class Lambda implements LambdaInterface { + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetadata('booking_id', 'booking_uuid'); - //Your Logic + const lambdaHandler = async (event: any, context: Context) => { + metrics.addMetadata('bookingId', '7051cd10-6283-11ec-90d6-0242ac120003'); } -} + + export const handler = middy(lambdaHandler) + .use(logMetrics(metrics)); ``` === "Example CloudWatch Logs excerpt" ```json hl_lines="23" { - "SuccessfulBooking": 1.0, + "successfulBooking": 1.0, "_aws": { "Timestamp": 1592234975665, "CloudWatchMetrics": [ { - "Namespace": "ExampleApplication", + "Namespace": "exampleApplication", "Dimensions": [ [ "service" @@ -316,7 +438,7 @@ class Lambda implements LambdaInterface { ], "Metrics": [ { - "Name": "SuccessfulBooking", + "Name": "successfulBooking", "Unit": "Count" } ] @@ -324,31 +446,64 @@ class Lambda implements LambdaInterface { ] }, "service": "booking", - "booking_id": "booking_uuid" + "bookingId": "7051cd10-6283-11ec-90d6-0242ac120003" } ``` -### Single metric with a different dimension +### Single metric with different dimensions CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetric` if you have a metric that should have different dimensions. !!! info - Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind: + For cost-efficiency, this feature would be used sparsely since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind: **unique metric = (metric_name + dimension_name + dimension_value)** +=== "logMetrics middleware" - ```typescript hl_lines="6-7" - class Lambda implements LambdaInterface { + ```typescript hl_lines="12 14 15" + import { Metrics, MetricUnits, logMetrics } from '@aws-lambda-powertools/metrics'; + import { Context } from 'aws-lambda'; + import middy from '@middy/core'; + + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); + + const lambdaHandler = async (event: any, context: Context) => { + metrics.addDimension('metricUnit', 'milliseconds'); + // This metric will have the "metricUnit" dimension, and no "metricType" dimension: + metrics.addMetric('latency', MetricUnits.Milliseconds, 56); + + const singleMetric = metrics.singleMetric(); + // 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, { captureColdStartMetric: true } })); + ``` + +=== "logMetrics decorator" + + ```typescript hl_lines="14 16 17" + import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + import { Context, Callback } from 'aws-lambda'; + + const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); + + export class MyFunction { @metrics.logMetrics() public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addDimension('metricUnit', 'milliseconds'); + // This metric will have the "metricUnit" dimension, and no "metricType" dimension: + metrics.addMetric('latency', MetricUnits.Milliseconds, 56); + const singleMetric = metrics.singleMetric(); - metrics.addDimension('OuterDimension', 'true'); - singleMetric.addDimension('InnerDimension', 'true'); - metrics.addMetric('test-metric', MetricUnits.Count, 10); - singleMetric.addMetric('single-metric', MetricUnits.Percent, 50); + // This metric will have the "metricType" dimension, and no "metricUnit" dimension: + singleMetric.addDimension('metricType', 'business'); + singleMetric.addMetric('orderSubmitted', MetricUnits.Count, 1); } } ``` diff --git a/packages/metrics/.eslintrc.json b/packages/metrics/.eslintrc.json deleted file mode 100644 index 2f6cfc14b9..0000000000 --- a/packages/metrics/.eslintrc.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "env": { - "jest": true, - "node": true - }, - "root": true, - "plugins": [ - "@typescript-eslint", - "import" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.es.json" - }, - "extends": [ - "plugin:import/typescript" - ], - "settings": { - "import/parsers": { - "@typescript-eslint/parser": [ - ".ts", - ".tsx" - ] - }, - "import/resolver": { - "node": {}, - "typescript": { - "project": "./tsconfig.es.json", - "alwaysTryTypes": true - } - } - }, - "ignorePatterns": [ - "*.js", - "!.projenrc.js", - "*.d.ts", - "node_modules/", - "*.generated.ts", - "coverage" - ], - "rules": { - "indent": [ - "off" - ], - "@typescript-eslint/indent": [ - "error", - 2 - ], - "quotes": [ - "error", - "single", - { - "avoidEscape": true - } - ], - "comma-dangle": [ - "error", - "always-multiline" - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "no-multi-spaces": [ - "error", - { - "ignoreEOLComments": false - } - ], - "array-bracket-spacing": [ - "error", - "never" - ], - "array-bracket-newline": [ - "error", - "consistent" - ], - "object-curly-spacing": [ - "error", - "always" - ], - "object-curly-newline": [ - "error", - { - "multiline": true, - "consistent": true - } - ], - "object-property-newline": [ - "error", - { - "allowAllPropertiesOnSameLine": true - } - ], - "keyword-spacing": [ - "error" - ], - "brace-style": [ - "error", - "1tbs", - { - "allowSingleLine": true - } - ], - "space-before-blocks": [ - "error" - ], - "curly": [ - "error", - "multi-line", - "consistent" - ], - "@typescript-eslint/member-delimiter-style": [ - "error" - ], - "semi": [ - "error", - "always" - ], - "max-len": [ - "error", - { - "code": 150, - "ignoreUrls": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreComments": true, - "ignoreRegExpLiterals": true - } - ], - "quote-props": [ - "error", - "consistent-as-needed" - ], - "@typescript-eslint/no-require-imports": [ - "error" - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "**/tests/**", - "**/build-tools/**" - ], - "optionalDependencies": false, - "peerDependencies": true - } - ], - "import/no-unresolved": [ - "error" - ], - "import/order": [ - "warn", - { - "groups": [ - "builtin", - "external" - ], - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ], - "no-duplicate-imports": [ - "error" - ], - "no-shadow": [ - "off" - ], - "@typescript-eslint/no-shadow": [ - "error" - ], - "key-spacing": [ - "error" - ], - "no-multiple-empty-lines": [ - "error" - ], - "@typescript-eslint/no-floating-promises": [ - "error" - ], - "no-return-await": [ - "off" - ], - "@typescript-eslint/return-await": [ - "error" - ], - "no-trailing-spaces": [ - "error" - ], - "dot-notation": [ - "error" - ], - "no-bitwise": [ - "error" - ], - "@typescript-eslint/member-ordering": [ - "error", - { - "default": [ - "public-static-field", - "public-static-method", - "protected-static-field", - "protected-static-method", - "private-static-field", - "private-static-method", - "field", - "constructor", - "method" - ] - } - ] - }, - "overrides": [ - { - "files": [ - ".projenrc.js" - ], - "rules": { - "@typescript-eslint/no-require-imports": "off", - "import/no-extraneous-dependencies": "off" - } - } - ] -} diff --git a/packages/metrics/README.md b/packages/metrics/README.md index c2f5ae9a63..7b823d4b98 100644 --- a/packages/metrics/README.md +++ b/packages/metrics/README.md @@ -24,7 +24,7 @@ Metrics has two global settings that will be used across all metrics emitted: |Setting|Description|Environment Variable|Constructor Parameter| |---|---|---|---| -|Metric namespace|Logical container where all metrics will be placed e.g. ServerlessAirline|POWERTOOLS_METRICS_NAMESPACE|namespace| +|Metric namespace|Logical container where all metrics will be placed e.g. serverlessAirline|POWERTOOLS_METRICS_NAMESPACE|namespace| |Service|Optionally, sets service metric dimension across all metrics e.g. payment|POWERTOOLS_SERVICE_NAME|service| ```typescript diff --git a/packages/metrics/examples/cold-start.ts b/packages/metrics/examples/cold-start.ts index 693e8d5bba..be861e795b 100644 --- a/packages/metrics/examples/cold-start.ts +++ b/packages/metrics/examples/cold-start.ts @@ -1,9 +1,9 @@ import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; import { populateEnvironmentVariables } from '../tests/helpers'; -import { Callback, Context } from 'aws-lambda/handler'; import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); @@ -12,15 +12,15 @@ process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; const metrics = new Metrics(); -class Lambda implements LambdaInterface { +const lambdaHandler = async (): Promise => { - @metrics.logMetrics({ captureColdStartMetric: true }) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addDimension('OuterDimension', 'true'); - metrics.addMetric('test-metric', MetricUnits.Count, 10); - } + metrics.addDimension('custom-dimension', 'true'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); -} +}; -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked again!')); \ No newline at end of file +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics, { captureColdStartMetric: true })); + +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked again!')); \ No newline at end of file diff --git a/packages/metrics/examples/constructor-options.ts b/packages/metrics/examples/constructor-options.ts index 67be0ace59..2bc994ee15 100644 --- a/packages/metrics/examples/constructor-options.ts +++ b/packages/metrics/examples/constructor-options.ts @@ -1,21 +1,25 @@ import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { populateEnvironmentVariables } from '../tests/helpers'; import { Metrics, MetricUnits } from '../src'; -import { LambdaInterface } from "./utils/lambda"; -import { Callback, Context } from "aws-lambda/handler"; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; const metrics = new Metrics({ namespace: 'hello-world-constructor', service: 'hello-world-service-constructor' }); -class Lambda implements LambdaInterface { - - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetric('test-metric', MetricUnits.Count, 10); +const lambdaHandler = async (): Promise => { + metrics.addMetric('test-metric', MetricUnits.Count, 10); +}; - } +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); -} -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/cold-start.ts b/packages/metrics/examples/decorator/cold-start.ts new file mode 100644 index 0000000000..de005cf9a0 --- /dev/null +++ b/packages/metrics/examples/decorator/cold-start.ts @@ -0,0 +1,26 @@ +import * as dummyEvent from '../../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './../utils/lambda/LambdaInterface'; +import { populateEnvironmentVariables } from '../../tests/helpers'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../../src'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics({ captureColdStartMetric: true }) + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addDimension('OuterDimension', 'true'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked again!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/constructor-options.ts b/packages/metrics/examples/decorator/constructor-options.ts new file mode 100644 index 0000000000..b95ea09b48 --- /dev/null +++ b/packages/metrics/examples/decorator/constructor-options.ts @@ -0,0 +1,21 @@ +import * as dummyEvent from '../../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../../tests/resources/contexts/hello-world'; +import { Metrics, MetricUnits } from '../../src'; +import { LambdaInterface } from './../utils/lambda'; +import { Callback, Context } from 'aws-lambda/handler'; + +const metrics = new Metrics({ + namespace: 'hello-world-constructor', + service: 'hello-world-service-constructor' +}); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics() + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addMetric('test-metric', MetricUnits.Count, 10); + + } + +} +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/default-dimensions-constructor.ts b/packages/metrics/examples/decorator/default-dimensions-constructor.ts new file mode 100644 index 0000000000..bbd8993e4a --- /dev/null +++ b/packages/metrics/examples/decorator/default-dimensions-constructor.ts @@ -0,0 +1,27 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world-constructor'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world-service-constructor'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../src'; + +const metrics = new Metrics({ defaultDimensions:{ 'application': 'hello-world' } }); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics() + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addMetric('test-metric', MetricUnits.Count, 10); + + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/default-dimensions.ts b/packages/metrics/examples/decorator/default-dimensions.ts new file mode 100644 index 0000000000..b08b4fbae9 --- /dev/null +++ b/packages/metrics/examples/decorator/default-dimensions.ts @@ -0,0 +1,34 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../src'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics({ defaultDimensions:{ 'application': 'hello-world' } }) + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + + metrics.addDimension('environment', 'dev'); + metrics.addDimension('application', 'hello-world-dev'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); + // You can override the default dimensions by clearing the existing metrics first. Note that the cleared metric will be dropped, it will NOT be published to CloudWatch + const metricsObject = metrics.serializeMetrics(); + metrics.clearMetrics(); + metrics.clearDimensions(); + metrics.addMetric('new-test-metric', MetricUnits.Count, 5); + console.log(JSON.stringify(metricsObject)); + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/dimensions.ts b/packages/metrics/examples/decorator/dimensions.ts new file mode 100644 index 0000000000..6c2289237c --- /dev/null +++ b/packages/metrics/examples/decorator/dimensions.ts @@ -0,0 +1,28 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../src'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics() + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + + metrics.addDimension('environment', 'dev'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); + + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/empty-metrics.ts b/packages/metrics/examples/decorator/empty-metrics.ts new file mode 100644 index 0000000000..91be760dc6 --- /dev/null +++ b/packages/metrics/examples/decorator/empty-metrics.ts @@ -0,0 +1,27 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics } from '../src'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + // Be default, we will not throw any error if there is no metrics. Use this property to override and throw an exception + @metrics.logMetrics({ raiseOnEmptyMetrics: true }) + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + // Notice that no metrics are added + // Since the raiseOnEmptyMetrics parameter is set to true, the Powertool throw an Error + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/hello-world.ts b/packages/metrics/examples/decorator/hello-world.ts new file mode 100644 index 0000000000..f522180e30 --- /dev/null +++ b/packages/metrics/examples/decorator/hello-world.ts @@ -0,0 +1,27 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world-service'; + +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../src'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics() + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addMetric('test-metric', MetricUnits.Count, 10); + + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/decorator/manual-flushing.ts b/packages/metrics/examples/decorator/manual-flushing.ts new file mode 100644 index 0000000000..a0a9c8f26f --- /dev/null +++ b/packages/metrics/examples/decorator/manual-flushing.ts @@ -0,0 +1,27 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = '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 { Metrics, MetricUnits } from '../src'; + +const metrics = new Metrics(); + +const lambdaHandler: Handler = async () => { + + metrics.addMetric('test-metric', MetricUnits.Count, 10); + metrics.purgeStoredMetrics(); + //Metrics will be published and cleared + + return { + foo: 'bar' + }; + +}; + +lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/programatic-access.ts b/packages/metrics/examples/decorator/manual-metrics-print.ts similarity index 92% rename from packages/metrics/examples/programatic-access.ts rename to packages/metrics/examples/decorator/manual-metrics-print.ts index aaea1e1925..0fbae05516 100644 --- a/packages/metrics/examples/programatic-access.ts +++ b/packages/metrics/examples/decorator/manual-metrics-print.ts @@ -1,4 +1,4 @@ -import { populateEnvironmentVariables } from '../tests/helpers'; +import { populateEnvironmentVariables } from '../../tests/helpers'; // Populate runtime populateEnvironmentVariables(); diff --git a/packages/metrics/examples/decorator/single-metric.ts b/packages/metrics/examples/decorator/single-metric.ts new file mode 100644 index 0000000000..0e0c84c6c8 --- /dev/null +++ b/packages/metrics/examples/decorator/single-metric.ts @@ -0,0 +1,31 @@ +import { populateEnvironmentVariables } from '../../tests/helpers'; +import * as dummyEvent from '../../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../../tests/resources/contexts/hello-world'; +import { LambdaInterface } from './../utils/lambda/LambdaInterface'; +import { Callback, Context } from 'aws-lambda/handler'; +import { Metrics, MetricUnits } from '../../src'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +const metrics = new Metrics(); + +class Lambda implements LambdaInterface { + + @metrics.logMetrics() + public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + metrics.addDimension('metricUnit', 'milliseconds'); + // This metric will have the "metricUnit" dimension, and no "metricType" dimension: + metrics.addMetric('latency', MetricUnits.Milliseconds, 56); + + const singleMetric = metrics.singleMetric(); + // This metric will have the "metricType" dimension, and no "metricUnit" dimension: + singleMetric.addDimension('metricType', 'business'); + singleMetric.addMetric('videoClicked', MetricUnits.Count, 1); + } + +} + +new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/default-dimensions-constructor.ts b/packages/metrics/examples/default-dimensions-constructor.ts index cb01c59c61..651139cfd9 100644 --- a/packages/metrics/examples/default-dimensions-constructor.ts +++ b/packages/metrics/examples/default-dimensions-constructor.ts @@ -1,27 +1,22 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables -process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world-constructor'; -process.env.POWERTOOLS_SERVICE_NAME = 'hello-world-service-constructor'; - -import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; -import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; -import { Metrics, MetricUnits } from '../src'; - -const metrics = new Metrics({ defaultDimensions:{ 'application': 'hello-world' } }); - -class Lambda implements LambdaInterface { +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetric('test-metric', MetricUnits.Count, 10); +const metrics = new Metrics({ defaultDimensions:{ 'application': 'my-application' } }); - } +const lambdaHandler = async (): Promise => { + metrics.addMetric('test-metric', MetricUnits.Count, 10); +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/default-dimensions.ts b/packages/metrics/examples/default-dimensions.ts index 9c8a06ddcb..aeb04a2e8b 100644 --- a/packages/metrics/examples/default-dimensions.ts +++ b/packages/metrics/examples/default-dimensions.ts @@ -1,33 +1,31 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; -import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; -import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; -import { Metrics, MetricUnits } from '../src'; - const metrics = new Metrics(); -class Lambda implements LambdaInterface { +const lambdaHandler = async (): Promise => { + metrics.addDimension('environment', 'dev'); + metrics.addDimension('application', 'hello-world-dev'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); + metrics.addMetric('new-test-metric-with-dimensions', MetricUnits.Count, 5); + metrics.addMetric('new-test-metric-without-dimensions', MetricUnits.Count, 5); - @metrics.logMetrics({ defaultDimensions:{ 'application': 'hello-world' } }) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { + // Optional: clear metrics and dimensions created till now + // metrics.clearMetrics(); + // metrics.clearDimensions(); - metrics.addDimension('environment', 'dev'); - metrics.addDimension('application', 'hello-world-dev'); - metrics.addMetric('test-metric', MetricUnits.Count, 10); - const metricsObject = metrics.serializeMetrics(); - metrics.clearMetrics(); - metrics.clearDimensions(); - metrics.addMetric('new-test-metric', MetricUnits.Count, 5); - console.log(JSON.stringify(metricsObject)); - } +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics, { defaultDimensions:{ 'application': 'hello-world' } })); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); diff --git a/packages/metrics/examples/dimensions.ts b/packages/metrics/examples/dimensions.ts index 0058344517..b59066ad6d 100644 --- a/packages/metrics/examples/dimensions.ts +++ b/packages/metrics/examples/dimensions.ts @@ -1,28 +1,23 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; -import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; -import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; -import { Metrics, MetricUnits } from '../src'; - -const metrics = new Metrics(); - -class Lambda implements LambdaInterface { - - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - - metrics.addDimension('environment', 'dev'); - metrics.addMetric('test-metric', MetricUnits.Count, 10); +const metrics = new Metrics({ defaultDimensions:{ 'application': 'my-application' } }); - } +const lambdaHandler = async (): Promise => { + metrics.addDimension('environment', 'dev'); + metrics.addMetric('test-metric', MetricUnits.Count, 10); +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/empty-metrics.ts b/packages/metrics/examples/empty-metrics.ts index 3c9a4ac63d..00c6e60870 100644 --- a/packages/metrics/examples/empty-metrics.ts +++ b/packages/metrics/examples/empty-metrics.ts @@ -1,25 +1,23 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; -import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; -import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; -import { Metrics } from '../src'; - const metrics = new Metrics(); -class Lambda implements LambdaInterface { - - @metrics.logMetrics({ raiseOnEmptyMetrics: true }) - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - - } +const lambdaHandler = async (): Promise => { + // Notice that no metrics are added + // Since the raiseOnEmptyMetrics parameter is set to true, the Powertool throw an Error +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics, { raiseOnEmptyMetrics: true })); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/hello-world.ts b/packages/metrics/examples/hello-world.ts index 088cb0a0c4..d001602de3 100644 --- a/packages/metrics/examples/hello-world.ts +++ b/packages/metrics/examples/hello-world.ts @@ -1,27 +1,22 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; -process.env.POWERTOOLS_SERVICE_NAME = 'hello-world-service'; - -import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; -import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; -import { Metrics, MetricUnits } from '../src'; const metrics = new Metrics(); -class Lambda implements LambdaInterface { - - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - metrics.addMetric('test-metric', MetricUnits.Count, 10); - - } +const lambdaHandler = async (): Promise => { + metrics.addMetric('test-metric', MetricUnits.Count, 10); +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics})); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/manual-flushing.ts b/packages/metrics/examples/manual-flushing.ts index 3e4b0621dc..5c978930b4 100644 --- a/packages/metrics/examples/manual-flushing.ts +++ b/packages/metrics/examples/manual-flushing.ts @@ -1,27 +1,23 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); // Additional runtime variables process.env.POWERTOOLS_METRICS_NAMESPACE = '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 { Metrics, MetricUnits } from '../src'; - const metrics = new Metrics(); -const lambdaHandler: Handler = async () => { - +const lambdaHandler = async (): Promise => { metrics.addMetric('test-metric', MetricUnits.Count, 10); metrics.purgeStoredMetrics(); - //Metrics will be logged and cleared - - return { - foo: 'bar' - }; - }; -lambdaHandler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); + +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/manual-metrics-print.ts b/packages/metrics/examples/manual-metrics-print.ts new file mode 100644 index 0000000000..b461f69ae7 --- /dev/null +++ b/packages/metrics/examples/manual-metrics-print.ts @@ -0,0 +1,25 @@ +import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; +import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; +import { populateEnvironmentVariables } from '../tests/helpers'; +import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; + +// Populate runtime +populateEnvironmentVariables(); +// Additional runtime variables +process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; + +const metrics = new Metrics(); + +const lambdaHandler = async (): Promise => { + metrics.addMetric('test-metric', MetricUnits.Count, 10); + const metricsObject = metrics.serializeMetrics(); + metrics.clearMetrics(); + console.log(JSON.stringify(metricsObject)); +}; + +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); + +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/examples/single-metric.ts b/packages/metrics/examples/single-metric.ts index c19ba4aa5e..78f9d399d3 100644 --- a/packages/metrics/examples/single-metric.ts +++ b/packages/metrics/examples/single-metric.ts @@ -1,9 +1,9 @@ -import { populateEnvironmentVariables } from '../tests/helpers'; import * as dummyEvent from '../../../tests/resources/events/custom/hello-world.json'; import { context as dummyContext } from '../../../tests/resources/contexts/hello-world'; -import { LambdaInterface } from './utils/lambda/LambdaInterface'; -import { Callback, Context } from 'aws-lambda/handler'; +import { populateEnvironmentVariables } from '../tests/helpers'; import { Metrics, MetricUnits } from '../src'; +import middy from '@middy/core'; +import { logMetrics } from '../src/middleware/middy'; // Populate runtime populateEnvironmentVariables(); @@ -12,17 +12,18 @@ process.env.POWERTOOLS_METRICS_NAMESPACE = 'hello-world'; const metrics = new Metrics(); -class Lambda implements LambdaInterface { +const lambdaHandler = async (): Promise => { + metrics.addDimension('metricUnit', 'milliseconds'); + // This metric will have the "metricUnit" dimension, and no "metricType" dimension: + metrics.addMetric('latency', MetricUnits.Milliseconds, 56); - @metrics.logMetrics() - public handler(_event: TEvent, _context: Context, _callback: Callback): void | Promise { - const singleMetric = metrics.singleMetric(); - metrics.addDimension('OuterDimension', 'true'); - singleMetric.addDimension('InnerDimension', 'true'); - metrics.addMetric('test-metric', MetricUnits.Count, 10); - singleMetric.addMetric('single-metric', MetricUnits.Percent, 50); - } + const singleMetric = metrics.singleMetric(); + // This metric will have the "metricType" dimension, and no "metricUnit" dimension: + singleMetric.addDimension('metricType', 'business'); + singleMetric.addMetric('videoClicked', MetricUnits.Count, 1); +}; -} +const handlerWithMiddleware = middy(lambdaHandler) + .use(logMetrics(metrics)); -new Lambda().handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file +handlerWithMiddleware(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); \ No newline at end of file diff --git a/packages/metrics/lerna b/packages/metrics/lerna deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/metrics/package-lock.json b/packages/metrics/package-lock.json index cd2c014daa..70b1eb39d1 100644 --- a/packages/metrics/package-lock.json +++ b/packages/metrics/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@aws-lambda-powertools/metrics", - "version": "0.1.1-beta.0", + "version": "0.2.0-beta.3", "license": "MIT-0", "dependencies": { "@aws-lambda-powertools/commons": "^0.0.2", @@ -19,6 +19,7 @@ "@aws-cdk/core": "^1.136.0", "@aws-lambda-powertools/commons": "^0.0.2", "@commitlint/cli": "^15.0.0", + "@middy/core": "^2.5.3", "@types/aws-lambda": "^8.10.72", "@types/jest": "^27.0.0", "@types/node": "^16.6.0", @@ -2072,6 +2073,15 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@middy/core": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@middy/core/-/core-2.5.3.tgz", + "integrity": "sha512-hCXlRglDu48sl03IjDGjcJwfwElIPAf0fwBu08kqE80qpnouZ1hGH1lZbkbJjAhz6DkSR+79YArUQKUYaM2k1g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11431,6 +11441,12 @@ "chalk": "^4.0.0" } }, + "@middy/core": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@middy/core/-/core-2.5.3.tgz", + "integrity": "sha512-hCXlRglDu48sl03IjDGjcJwfwElIPAf0fwBu08kqE80qpnouZ1hGH1lZbkbJjAhz6DkSR+79YArUQKUYaM2k1g==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 9ac62daae8..ed7c052cfa 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -27,9 +27,19 @@ "example:empty-metrics": "ts-node examples/empty-metrics.ts", "example:single-metric": "ts-node examples/single-metric.ts", "example:cold-start": "ts-node examples/cold-start.ts", - "example:programatic-access": "ts-node examples/programatic-access.ts", + "example:manual-metrics-print": "ts-node examples/manual-metrics-print.ts", "example:constructor-options": "ts-node examples/constructor-options.ts", - "example:default-dimensions-constructor": "ts-node examples/default-dimensions-constructor.ts" + "example:default-dimensions-constructor": "ts-node examples/default-dimensions-constructor.ts", + "example:decorator-hello-world": "ts-node examples/decorator/hello-world.ts", + "example:decorator-manual-flushing": "ts-node examples/decorator/manual-flushing.ts", + "example:decorator-dimensions": "ts-node examples/decorator/dimensions.ts", + "example:decorator-default-dimensions": "ts-node examples/decorator/default-dimensions.ts", + "example:decorator-empty-metrics": "ts-node examples/decorator/empty-metrics.ts", + "example:decorator-single-metric": "ts-node examples/decorator/single-metric.ts", + "example:decorator-cold-start": "ts-node examples/decorator/cold-start.ts", + "example:decorator-manual-metrics-print": "ts-node examples/decorator/manual-metrics-print.ts", + "example:decorator-constructor-options": "ts-node examples/decorator/constructor-options.ts", + "example:decorator-default-dimensions-constructor": "ts-node examples/decorator/default-dimensions-constructor.ts" }, "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/master/packages/metrics#readme", "license": "MIT-0", @@ -42,6 +52,7 @@ "@aws-cdk/core": "^1.136.0", "@aws-lambda-powertools/commons": "^0.0.2", "@commitlint/cli": "^15.0.0", + "@middy/core": "^2.5.3", "@types/aws-lambda": "^8.10.72", "@types/jest": "^27.0.0", "@types/node": "^16.6.0", diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index e61cd12b31..6a51589d37 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -1,12 +1,12 @@ import { MetricsInterface } from '.'; import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; import { - DecoratorOptions, + MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, StoredMetrics, - MetricsOptions, + ExtraOptions, MetricUnit, MetricUnits, } from './types'; @@ -149,6 +149,41 @@ class Metrics implements MetricsInterface { if (this.isSingleMetric) this.purgeStoredMetrics(); } + /** + * Create a singleMetric to capture cold start. + * If it's a cold start invocation, this feature will: + * * Create a separate EMF blob solely containing a metric named ColdStart + * * Add function_name and service dimensions + * + * This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions. + * + * @example + * + * ```typescript + * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + * import { Context } from 'aws-lambda'; + * + * const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); + * + * export const handler = async (event: any, context: Context) => { + * metrics.captureColdStartMetric(); + * } + * ``` + */ + public captureColdStartMetric(): void { + if (!this.isColdStart) return; + this.isColdStart = false; + const singleMetric = this.singleMetric(); + + if (this.dimensions.service) { + singleMetric.addDimension('service', this.dimensions.service); + } + if (this.functionName != null) { + singleMetric.addDimension('function_name', this.functionName); + } + singleMetric.addMetric('ColdStart', MetricUnits.Count, 1); + } + public clearDefaultDimensions(): void { this.defaultDimensions = {}; } @@ -165,28 +200,6 @@ class Metrics implements MetricsInterface { this.storedMetrics = {}; } - - /** - * Throw an Error if the metrics buffer is empty. - * - * @example - * - * ```typescript - * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - * import { Context } from 'aws-lambda'; - * - * const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); - * - * export const handler = async (event: any, context: Context) => { - * metrics.raiseOnEmptyMetrics(); - * metrics.purgeStoredMetrics(); // will throw since no metrics added. - * } - * ``` - */ - public raiseOnEmptyMetrics(): void { - this.shouldRaiseOnEmptyMetrics = true; - } - /** * A decorator automating coldstart capture, raise on empty metrics and publishing metrics on handler exit. * @@ -212,7 +225,7 @@ class Metrics implements MetricsInterface { * * @decorator Class */ - public logMetrics(options: DecoratorOptions = {}): HandlerMethodDecorator { + public logMetrics(options: ExtraOptions = {}): HandlerMethodDecorator { const { raiseOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (raiseOnEmptyMetrics) { this.raiseOnEmptyMetrics(); @@ -228,7 +241,8 @@ class Metrics implements MetricsInterface { if (captureColdStartMetric) this.captureColdStartMetric(); try { - const result = originalMethod?.apply(this, [event, context, callback]); + const result = originalMethod?.apply(this, [ event, context, callback ]); + return result; } finally { this.purgeStoredMetrics(); @@ -259,6 +273,27 @@ class Metrics implements MetricsInterface { this.storedMetrics = {}; } + /** + * Throw an Error if the metrics buffer is empty. + * + * @example + * + * ```typescript + * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; + * import { Context } from 'aws-lambda'; + * + * const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"}); + * + * export const handler = async (event: any, context: Context) => { + * metrics.raiseOnEmptyMetrics(); + * metrics.purgeStoredMetrics(); // will throw since no metrics added. + * } + * ``` + */ + public raiseOnEmptyMetrics(): void { + this.shouldRaiseOnEmptyMetrics = true; + } + /** * Function to create the right object compliant with Cloudwatch EMF (Event Metric Format). * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html for more details @@ -284,7 +319,7 @@ class Metrics implements MetricsInterface { {}, ); - const dimensionNames = [...Object.keys(this.defaultDimensions), ...Object.keys(this.dimensions)]; + const dimensionNames = [ ...Object.keys(this.defaultDimensions), ...Object.keys(this.dimensions) ]; return { _aws: { @@ -315,6 +350,10 @@ class Metrics implements MetricsInterface { this.defaultDimensions = targetDimensions; } + public setFunctionName(value: string): void { + this.functionName = value; + } + /** * CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric if you have a metric that should have different dimensions. * @@ -334,45 +373,11 @@ class Metrics implements MetricsInterface { return new Metrics({ namespace: this.namespace, service: this.dimensions.service, + defaultDimensions: this.defaultDimensions, singleMetric: true, }); } - /** - * Create a singleMetric to capture cold start. - * If it's a cold start invocation, this feature will: - * * Create a separate EMF blob solely containing a metric named ColdStart - * * Add function_name and service dimensions - * - * This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions. - * - * @example - * - * ```typescript - * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; - * import { Context } from 'aws-lambda'; - * - * const metrics = new Metrics({namespace:"ServerlessAirline", service:"orders"}); - * - * export const handler = async (event: any, context: Context) => { - * metrics.captureColdStartMetric(); - * } - * ``` - */ - public captureColdStartMetric(): void { - if (!this.isColdStart) return; - this.isColdStart = false; - const singleMetric = this.singleMetric(); - - if (this.dimensions.service) { - singleMetric.addDimension('service', this.dimensions.service); - } - if (this.functionName != null) { - singleMetric.addDimension('function_name', this.functionName); - } - singleMetric.addMetric('ColdStart', MetricUnits.Count, 1); - } - private getCurrentDimensionsCount(): number { return Object.keys(this.dimensions).length + Object.keys(this.defaultDimensions).length; } diff --git a/packages/metrics/src/MetricsInterface.ts b/packages/metrics/src/MetricsInterface.ts index 5375003032..00e26ba8aa 100644 --- a/packages/metrics/src/MetricsInterface.ts +++ b/packages/metrics/src/MetricsInterface.ts @@ -1,20 +1,20 @@ import { Metrics } from './Metrics'; -import { MetricUnit, EmfOutput, HandlerMethodDecorator, Dimensions, DecoratorOptions } from './types'; +import { MetricUnit, EmfOutput, HandlerMethodDecorator, Dimensions, MetricsOptions } from './types'; interface MetricsInterface { - addDimension(name: string, value: string): void; - addDimensions(dimensions: {[key: string]: string}): void; - addMetadata(key: string, value: string): void; - addMetric(name: string, unit:MetricUnit, value:number): void; - clearDimensions(): void; - clearMetadata(): void; - clearMetrics(): void; - clearDefaultDimensions(): void; - logMetrics(options?: DecoratorOptions): HandlerMethodDecorator; - purgeStoredMetrics(): void; - serializeMetrics(): EmfOutput; - setDefaultDimensions(dimensions: Dimensions | undefined): void; - singleMetric(): Metrics; + addDimension(name: string, value: string): void + addDimensions(dimensions: {[key: string]: string}): void + addMetadata(key: string, value: string): void + addMetric(name: string, unit:MetricUnit, value:number): void + clearDimensions(): void + clearMetadata(): void + clearMetrics(): void + clearDefaultDimensions(): void + logMetrics(options?: MetricsOptions): HandlerMethodDecorator + purgeStoredMetrics(): void + serializeMetrics(): EmfOutput + setDefaultDimensions(dimensions: Dimensions | undefined): void + singleMetric(): Metrics } export { diff --git a/packages/metrics/src/config/ConfigService.ts b/packages/metrics/src/config/ConfigService.ts index fec8169e2a..a1fef20267 100644 --- a/packages/metrics/src/config/ConfigService.ts +++ b/packages/metrics/src/config/ConfigService.ts @@ -2,9 +2,9 @@ import { ConfigServiceInterface } from '.'; abstract class ConfigService implements ConfigServiceInterface { - abstract get(name: string): string; - abstract getNamespace(): string; - abstract getService(): string; + public abstract get(name: string): string; + public abstract getNamespace(): string; + public abstract getService(): string; } diff --git a/packages/metrics/src/config/ConfigServiceInterface.ts b/packages/metrics/src/config/ConfigServiceInterface.ts index be86bc651d..65406c5037 100644 --- a/packages/metrics/src/config/ConfigServiceInterface.ts +++ b/packages/metrics/src/config/ConfigServiceInterface.ts @@ -1,8 +1,8 @@ interface ConfigServiceInterface { - get?(name: string): string; - getNamespace(): string; - getService(): string; + get?(name: string): string + getNamespace(): string + getService(): string } diff --git a/packages/metrics/src/middleware/index.ts b/packages/metrics/src/middleware/index.ts new file mode 100644 index 0000000000..fcc794fc08 --- /dev/null +++ b/packages/metrics/src/middleware/index.ts @@ -0,0 +1 @@ +export * from './middy'; \ No newline at end of file diff --git a/packages/metrics/src/middleware/middy.ts b/packages/metrics/src/middleware/middy.ts new file mode 100644 index 0000000000..6c9a077d3b --- /dev/null +++ b/packages/metrics/src/middleware/middy.ts @@ -0,0 +1,40 @@ +import type { Metrics } from '../Metrics'; +import middy from '@middy/core'; +import { ExtraOptions } from '../types'; + +const logMetrics = (target: Metrics | Metrics[], options: ExtraOptions = {}): middy.MiddlewareObj => { + const metricsInstances = target instanceof Array ? target : [target]; + + const logMetricsBefore = async (request: middy.Request): Promise => { + metricsInstances.forEach((metrics: Metrics) => { + metrics.setFunctionName(request.context.functionName); + const { raiseOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; + if (raiseOnEmptyMetrics !== undefined) { + metrics.raiseOnEmptyMetrics(); + } + if (defaultDimensions !== undefined) { + metrics.setDefaultDimensions(defaultDimensions); + } + if (captureColdStartMetric !== undefined) { + metrics.captureColdStartMetric(); + } + }); + + }; + + const logMetricsAfterOrError = async (): Promise => { + metricsInstances.forEach((metrics: Metrics) => { + metrics.purgeStoredMetrics(); + }); + }; + + return { + before: logMetricsBefore, + after: logMetricsAfterOrError, + onError: logMetricsAfterOrError + }; +}; + +export { + logMetrics, +}; \ No newline at end of file diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index d3f7219b0d..0ceb2bc551 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -6,23 +6,23 @@ import { MetricUnit } from './MetricUnit'; type Dimensions = { [key: string]: string }; type MetricsOptions = { - customConfigService?: ConfigServiceInterface; - namespace?: string; - service?: string; - singleMetric?: boolean; - defaultDimensions?: Dimensions; + customConfigService?: ConfigServiceInterface + namespace?: string + service?: string + singleMetric?: boolean + defaultDimensions?: Dimensions }; type EmfOutput = { - [key: string]: string | number | object; + [key: string]: string | number | object _aws: { - Timestamp: number; + Timestamp: number CloudWatchMetrics: { - Namespace: string; - Dimensions: [string[]]; - Metrics: { Name: string; Unit: MetricUnit }[]; - }[]; - }; + Namespace: string + Dimensions: [string[]] + Metrics: { Name: string; Unit: MetricUnit }[] + }[] + } }; type HandlerMethodDecorator = ( @@ -38,7 +38,7 @@ type HandlerMethodDecorator = ( * * ```typescript * - * const metricsOptions: DecoratorOptions = { + * const metricsOptions: MetricsOptions = { * raiseOnEmptyMetrics: true, * defaultDimensions: {'environment': 'dev'}, * captureColdStartMetric: true, @@ -50,20 +50,20 @@ type HandlerMethodDecorator = ( * } * ``` */ -type DecoratorOptions = { - raiseOnEmptyMetrics?: boolean; - defaultDimensions?: Dimensions; - captureColdStartMetric?: boolean; +type ExtraOptions = { + raiseOnEmptyMetrics?: boolean + defaultDimensions?: Dimensions + captureColdStartMetric?: boolean }; type StoredMetric = { - name: string; - unit: MetricUnit; - value: number; + name: string + unit: MetricUnit + value: number }; type StoredMetrics = { - [key: string]: StoredMetric; + [key: string]: StoredMetric }; -export { DecoratorOptions, Dimensions, EmfOutput, HandlerMethodDecorator, MetricsOptions, StoredMetrics }; +export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics }; diff --git a/packages/metrics/tests/e2e/standardFunctions.test.MyFunction.ts b/packages/metrics/tests/e2e/standardFunctions.test.MyFunction.ts index 519e49cacd..a848e71416 100644 --- a/packages/metrics/tests/e2e/standardFunctions.test.MyFunction.ts +++ b/packages/metrics/tests/e2e/standardFunctions.test.MyFunction.ts @@ -1,4 +1,5 @@ import { Metrics, MetricUnits } from '../../src'; +import { Context } from 'aws-lambda'; const namespace = process.env.EXPECTED_NAMESPACE ?? 'CDKExample'; const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; @@ -14,7 +15,7 @@ const singleMetricValue = process.env.EXPECTED_SINGLE_METRIC_VALUE ?? 2; const metrics = new Metrics({ namespace: namespace, service: serviceName }); -export const handler = async (event: any, context: any) => { +export const handler = async (_event: unknown, _context: Context): Promise => { metrics.captureColdStartMetric(); metrics.raiseOnEmptyMetrics(); metrics.setDefaultDimensions(JSON.parse(defaultDimensions)); diff --git a/packages/metrics/tests/e2e/standardFunctions.test.ts b/packages/metrics/tests/e2e/standardFunctions.test.ts index a4e6a72e36..76b738501d 100644 --- a/packages/metrics/tests/e2e/standardFunctions.test.ts +++ b/packages/metrics/tests/e2e/standardFunctions.test.ts @@ -10,7 +10,7 @@ import { randomUUID } from 'crypto'; import { Tracing } from '@aws-cdk/aws-lambda'; import * as lambda from '@aws-cdk/aws-lambda-nodejs'; -import { App, Stack, CfnOutput } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import { SdkProvider } from 'aws-cdk/lib/api/aws-auth'; import { CloudFormationDeployments } from 'aws-cdk/lib/api/cloudformation-deployments'; import * as AWS from 'aws-sdk'; @@ -38,7 +38,7 @@ describe('coldstart', () => { const expectedSingleMetricUnit = MetricUnits.Percent; const expectedSingleMetricValue = '2'; const functionName = 'MyFunctionWithStandardHandler'; - const myFunctionWithStandardFunctions = new lambda.NodejsFunction(stack, 'MyFunction', { + new lambda.NodejsFunction(stack, 'MyFunction', { functionName: functionName, tracing: Tracing.ACTIVE, environment: { diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 4eacb26a7f..2e348f39f5 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -1,11 +1,13 @@ /** - * Test metrics decorator + * Test Metrics class * - * @group unit/metrics/all + * @group unit/metrics/class */ import { ContextExamples as dummyContext, LambdaInterface } from '@aws-lambda-powertools/commons'; import { Context, Callback } from 'aws-lambda'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import * as dummyEvent from '../../../../tests/resources/events/custom/hello-world.json'; import { Metrics, MetricUnits } from '../../src/'; import { populateEnvironmentVariables } from '../helpers'; @@ -17,9 +19,15 @@ const DEFAULT_NAMESPACE = 'default_namespace'; const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); interface LooseObject { - [key: string]: string; + [key: string]: string } +type DummyEvent = { + key1: string + key2: string + key3: string +}; + describe('Class: Metrics', () => { const originalEnvironmentVariables = process.env; @@ -233,7 +241,7 @@ describe('Class: Metrics', () => { test('Cold start metric should only be written out once and flushed automatically', async () => { const metrics = new Metrics({ namespace: 'test' }); - const handler = async (event: any, context: Context) => { + const handler = async (_event: DummyEvent, _context: Context): Promise => { // Should generate only one log metrics.captureColdStartMetric(); }; @@ -267,7 +275,7 @@ describe('Class: Metrics', () => { await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!')); await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked again!')); - const loggedData = [JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0])]; + const loggedData = [ JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0]) ]; expect(console.log).toBeCalledTimes(3); expect(loggedData[0]._aws.CloudWatchMetrics[0].Metrics.length).toBe(1); @@ -367,7 +375,7 @@ describe('Class: Metrics', () => { expect.assertions(1); const metrics = new Metrics({ namespace: 'test' }); - const handler = async (event: any, context: Context) => { + const handler = async (_event: DummyEvent, _context: Context): Promise => { metrics.raiseOnEmptyMetrics(); // Logic goes here metrics.purgeStoredMetrics(); @@ -401,7 +409,7 @@ describe('Class: Metrics', () => { } await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!')); - const loggedData = [JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0])]; + const loggedData = [ JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0]) ]; expect(console.log).toBeCalledTimes(2); expect(loggedData[0]._aws.CloudWatchMetrics[0].Metrics.length).toBe(100); @@ -410,7 +418,14 @@ describe('Class: Metrics', () => { }); describe('Feature: Output validation ', () => { - test('Should use default namespace if no namepace is set', () => { + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + beforeEach(() => { + consoleSpy.mockClear(); + }); + + test('Should use default namespace if no namespace is set', () => { delete process.env.POWERTOOLS_METRICS_NAMESPACE; const metrics = new Metrics(); @@ -418,6 +433,7 @@ describe('Class: Metrics', () => { const serializedMetrics = metrics.serializeMetrics(); expect(serializedMetrics._aws.CloudWatchMetrics[0].Namespace).toBe(DEFAULT_NAMESPACE); + expect(console.warn).toHaveBeenNthCalledWith(1, 'Namespace should be defined, default used'); }); }); @@ -463,7 +479,7 @@ describe('Class: Metrics', () => { } await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!')); - const loggedData = [JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0])]; + const loggedData = [ JSON.parse(consoleSpy.mock.calls[0][0]), JSON.parse(consoleSpy.mock.calls[1][0]) ]; expect(console.log).toBeCalledTimes(2); expect(loggedData[0]._aws.CloudWatchMetrics[0].Metrics.length).toBe(1); diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts new file mode 100644 index 0000000000..8a48c3b0e9 --- /dev/null +++ b/packages/metrics/tests/unit/middleware/middy.test.ts @@ -0,0 +1,173 @@ +/** + * Test metrics middleware + * + * @group unit/metrics/middleware + */ + +import { logMetrics } from '../../../../metrics/src/middleware'; +import { Metrics, MetricUnits } from '../../../../metrics/src'; +import middy from '@middy/core'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import * as event from '../../../../../tests/resources/events/custom/hello-world.json'; +import { ExtraOptions } from '../../../src/types'; + +const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); +const mockDate = new Date(1466424490000); +const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as unknown as string); + +describe('Middy middleware', () => { + + beforeEach(() => { + jest.resetModules(); + consoleSpy.mockClear(); + dateSpy.mockClear(); + }); + + describe('logMetrics', () => { + + const getRandomInt = (): number => Math.floor(Math.random() * 1000000000); + const awsRequestId = getRandomInt().toString(); + + const context = { + 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: awsRequestId, + getRemainingTimeInMillis: () => 1234, + done: () => console.log('Done!'), + fail: () => console.log('Failed!'), + succeed: () => console.log('Succeeded!'), + }; + + test('when a metrics instance is passed WITH custom options, it prints the metrics in the stdout', async () => { + + // Prepare + const metrics = new Metrics({ namespace:'serverlessAirline', service:'orders' }); + + const lambdaHandler = (): void => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + }; + const metricsOptions: ExtraOptions = { + raiseOnEmptyMetrics: true, + defaultDimensions: { environment : 'prod', aws_region: 'eu-central-1' }, + captureColdStartMetric: true + }; + const handler = middy(lambdaHandler).use(logMetrics(metrics, metricsOptions)); + + // Act + await handler(event, context, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toHaveBeenNthCalledWith(1, JSON.stringify({ + '_aws': { + 'Timestamp': 1466424490000, + 'CloudWatchMetrics': [{ + 'Namespace': 'serverlessAirline', + 'Dimensions': [ + [ 'environment', 'aws_region', 'service', 'function_name' ] + ], + 'Metrics': [{ 'Name': 'ColdStart', 'Unit': 'Count' }], + }], + }, + 'environment': 'prod', + 'aws_region' : 'eu-central-1', + 'service': 'orders', + 'function_name': 'foo-bar-function', + 'ColdStart': 1, + })); + expect(console.log).toHaveBeenNthCalledWith(2, JSON.stringify({ + '_aws': { + 'Timestamp': 1466424490000, + 'CloudWatchMetrics': [{ + 'Namespace': 'serverlessAirline', + 'Dimensions': [ + [ 'environment', 'aws_region', 'service' ] + ], + 'Metrics': [{ 'Name': 'successfulBooking', 'Unit': 'Count' }], + }], + }, + 'environment': 'prod', + 'aws_region' : 'eu-central-1', + 'service': 'orders', + 'successfulBooking': 1, + })); + + }); + + test('when a metrics instance is passed WITHOUT custom options, it prints the metrics in the stdout', async () => { + + // Prepare + const metrics = new Metrics({ namespace:'serverlessAirline', service:'orders' }); + + const lambdaHandler = (): void => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + }; + + const handler = middy(lambdaHandler).use(logMetrics(metrics)); + + // Act + await handler(event, context, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toHaveBeenNthCalledWith(1, JSON.stringify({ + '_aws': { + 'Timestamp': 1466424490000, + 'CloudWatchMetrics': [{ + 'Namespace': 'serverlessAirline', + 'Dimensions': [ + ['service'] + ], + 'Metrics': [{ 'Name': 'successfulBooking', 'Unit': 'Count' }], + }], + }, + 'service': 'orders', + 'successfulBooking': 1, + })); + + }); + + test('when an array of Metrics instances is passed, it prints the metrics in the stdout', async () => { + + // Prepare + const metrics = new Metrics({ namespace:'serverlessAirline', service:'orders' }); + + const lambdaHandler = (): void => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + }; + const metricsOptions: ExtraOptions = { + raiseOnEmptyMetrics: true + }; + const handler = middy(lambdaHandler).use(logMetrics([metrics], metricsOptions)); + + // Act + await handler(event, context, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toHaveBeenNthCalledWith(1, JSON.stringify({ + '_aws': { + 'Timestamp': 1466424490000, + 'CloudWatchMetrics': [{ + 'Namespace': 'serverlessAirline', + 'Dimensions': [ + ['service'] + ], + 'Metrics': [{ 'Name': 'successfulBooking', 'Unit': 'Count' }], + }], + }, + 'service': 'orders', + 'successfulBooking': 1, + })); + + }); + + }); + +}); \ No newline at end of file diff --git a/packages/metrics/tsconfig.json b/packages/metrics/tsconfig.json index 9bd3b0686e..582618277f 100644 --- a/packages/metrics/tsconfig.json +++ b/packages/metrics/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "pretty": true, "baseUrl": "src/", - "rootDirs": [ "src/" ] + "rootDirs": [ "src/" ], + "esModuleInterop": true }, "include": [ "src/**/*"], "exclude": [ "./node_modules"],