Skip to content

Commit

Permalink
feat(metrics): support high resolution metrics (#1369)
Browse files Browse the repository at this point in the history
  • Loading branch information
niko-achilles authored Mar 16, 2023
1 parent 8a47e76 commit 79a321b
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 26 deletions.
21 changes: 20 additions & 1 deletion docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar

* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
* **Metric**. It's the name of the metric, for example: SuccessfulBooking or UpdatedBooking.
* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: Count or Seconds.
* **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition).

<figure>
<img src="../../media/metrics_terminology.png" />
Expand Down Expand Up @@ -117,7 +120,23 @@ You can create metrics using the `addMetric` method, and you can create dimensio
CloudWatch EMF supports a max of 100 metrics per batch. Metrics will automatically propagate all the metrics when adding the 100th metric. Subsequent metrics, e.g. 101th, will be aggregated into a new EMF object, for your convenience.

!!! warning "Do not create metrics or dimensions outside the handler"
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behaviour.
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.

### Adding high-resolution metrics

You can create [high-resolution metrics](https://aws.amazon.com/about-aws/whats-new/2023/02/amazon-cloudwatch-high-resolution-metric-extraction-structured-logs/) passing `resolution` as parameter to `addMetric`.

!!! tip "When is it useful?"
High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.

=== "Metrics with high resolution"

```typescript hl_lines="6"
--8<-- "docs/snippets/metrics/addHighResolutionMetric.ts"
```

!!! tip "Autocomplete Metric Resolutions"
Use the `MetricResolution` type to easily find a supported metric resolution by CloudWatch. Alternatively, you can pass the allowed values of 1 or 60 as an integer.

### Adding multi-value metrics

Expand Down
7 changes: 7 additions & 0 deletions docs/snippets/metrics/addHighResolutionMetric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Metrics, MetricUnits, MetricResolution } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });

export const handler = async (_event: unknown, _context: unknown): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High);
};
2 changes: 1 addition & 1 deletion docs/snippets/metrics/basicUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });

export const handler = async (_event, _context): Promise<void> => {
export const handler = async (_event: unknown, _context: unknown): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
};
81 changes: 67 additions & 14 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
ExtraOptions,
MetricUnit,
MetricUnits,
MetricResolution,
MetricDefinition,
StoredMetric,
} from './types';

const MAX_METRICS_SIZE = 100;
Expand Down Expand Up @@ -165,12 +168,33 @@ class Metrics extends Utility implements MetricsInterface {

/**
* Add a metric to the metrics buffer.
* @param name
* @param unit
* @param value
*
* @example
*
* Add Metric using MetricUnit Enum supported by Cloudwatch
*
* ```ts
* metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
* ```
*
* @example
*
* Add Metric using MetricResolution type with resolutions High or Standard supported by cloudwatch
*
* ```ts
* metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High);
* ```
*
* @param name - The metric name
* @param unit - The metric unit
* @param value - The metric value
* @param resolution - The metric resolution
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition Amazon Cloudwatch Concepts Documentation
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html#CloudWatch_Embedded_Metric_Format_Specification_structure_metricdefinition Metric Definition of Embedded Metric Format Specification
*/
public addMetric(name: string, unit: MetricUnit, value: number): void {
this.storeMetric(name, unit, value);

public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = MetricResolution.Standard): void {
this.storeMetric(name, unit, value, resolution);
if (this.isSingleMetric) this.publishStoredMetrics();
}

Expand Down Expand Up @@ -314,15 +338,29 @@ class Metrics extends Utility implements MetricsInterface {
}

/**
* Function to create the right object compliant with Cloudwatch EMF (Event Metric Format).
* Function to create the right object compliant with Cloudwatch EMF (Embedded Metric Format).
*
*
* @returns metrics as JSON object compliant EMF Schema Specification
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html for more details
* @returns {string}
*/
public serializeMetrics(): EmfOutput {
const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({
Name: metricDefinition.name,
Unit: metricDefinition.unit,
}));
// For high-resolution metrics, add StorageResolution property
// Example: [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 1 } ]

// For standard resolution metrics, don't add StorageResolution property to avoid unnecessary ingestion of data into cloudwatch
// Example: [ { "Name": "metric_name", "Unit": "Count"} ]
const metricDefinitions: MetricDefinition[] = Object.values(this.storedMetrics).map((metricDefinition) =>
this.isHigh(metricDefinition['resolution'])
? ({
Name: metricDefinition.name,
Unit: metricDefinition.unit,
StorageResolution: metricDefinition.resolution
}): ({
Name: metricDefinition.name,
Unit: metricDefinition.unit,
}));

if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) {
throw new RangeError('The number of metrics recorded must be higher than zero');
}
Expand Down Expand Up @@ -429,6 +467,10 @@ class Metrics extends Utility implements MetricsInterface {
return <EnvironmentVariablesService> this.envVarsService;
}

private isHigh(resolution: StoredMetric['resolution']): resolution is typeof MetricResolution['High'] {
return resolution === MetricResolution.High;
}

private isNewMetric(name: string, unit: MetricUnit): boolean {
if (this.storedMetrics[name]){
// Inconsistent units indicates a bug or typos and we want to flag this to users early
Expand Down Expand Up @@ -479,7 +521,12 @@ class Metrics extends Utility implements MetricsInterface {
}
}

private storeMetric(name: string, unit: MetricUnit, value: number): void {
private storeMetric(
name: string,
unit: MetricUnit,
value: number,
resolution: MetricResolution,
): void {
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
this.publishStoredMetrics();
}
Expand All @@ -488,8 +535,10 @@ class Metrics extends Utility implements MetricsInterface {
this.storedMetrics[name] = {
unit,
value,
name,
name,
resolution
};

} else {
const storedMetric = this.storedMetrics[name];
if (!Array.isArray(storedMetric.value)) {
Expand All @@ -501,4 +550,8 @@ class Metrics extends Utility implements MetricsInterface {

}

export { Metrics, MetricUnits };
export {
Metrics,
MetricUnits,
MetricResolution,
};
12 changes: 9 additions & 3 deletions packages/metrics/src/MetricsInterface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Metrics } from './Metrics';
import { MetricUnit, EmfOutput, HandlerMethodDecorator, Dimensions, MetricsOptions } from './types';

import {
MetricUnit,
MetricResolution,
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
addMetric(name: string, unit:MetricUnit, value:number, resolution?: MetricResolution): void
clearDimensions(): void
clearMetadata(): void
clearMetrics(): void
Expand Down
8 changes: 8 additions & 0 deletions packages/metrics/src/types/MetricResolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const MetricResolution = {
Standard: 60,
High: 1,
} as const;

type MetricResolution = typeof MetricResolution[keyof typeof MetricResolution];

export { MetricResolution };
14 changes: 11 additions & 3 deletions packages/metrics/src/types/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Handler } from 'aws-lambda';
import { LambdaInterface, AsyncHandler, SyncHandler } from '@aws-lambda-powertools/commons';
import { ConfigServiceInterface } from '../config';
import { MetricUnit } from './MetricUnit';
import { MetricResolution } from './MetricResolution';

type Dimensions = { [key: string]: string };

Expand All @@ -19,8 +20,8 @@ type EmfOutput = {
Timestamp: number
CloudWatchMetrics: {
Namespace: string
Dimensions: [string[]]
Metrics: { Name: string; Unit: MetricUnit }[]
Dimensions: [string[]]
Metrics: MetricDefinition[]
}[]
}
};
Expand Down Expand Up @@ -60,10 +61,17 @@ type StoredMetric = {
name: string
unit: MetricUnit
value: number | number[]
resolution: MetricResolution
};

type StoredMetrics = {
[key: string]: StoredMetric
};

export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics };
type MetricDefinition = {
Name: string
Unit: MetricUnit
StorageResolution?: MetricResolution
};

export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics, StoredMetric, MetricDefinition };
3 changes: 2 additions & 1 deletion packages/metrics/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Metrics';
export * from './MetricUnit';
export * from './MetricUnit';
export * from './MetricResolution';
58 changes: 57 additions & 1 deletion packages/metrics/tests/unit/Metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import { ContextExamples as dummyContext, Events as dummyEvent, LambdaInterface } from '@aws-lambda-powertools/commons';
import { Context, Callback } from 'aws-lambda';
import { Metrics, MetricUnits } from '../../src/';

import { Metrics, MetricUnits, MetricResolution } from '../../src/';

const MAX_METRICS_SIZE = 100;
const MAX_DIMENSION_COUNT = 29;
Expand Down Expand Up @@ -563,6 +564,61 @@ describe('Class: Metrics', () => {
});
});

describe('Feature: Resolution of Metrics', ()=>{

test('serialized metrics in EMF format should not contain `StorageResolution` as key if none is set', () => {
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10);
const serializedMetrics = metrics.serializeMetrics();

expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');

});
test('serialized metrics in EMF format should not contain `StorageResolution` as key if `Standard` is set', () => {
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.Standard);
const serializedMetrics = metrics.serializeMetrics();

// expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.Standard);
// expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(60);

expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');
});

test('serialized metrics in EMF format should not contain `StorageResolution` as key if `60` is set',()=>{
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60);
const serializedMetrics = metrics.serializeMetrics();

expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');
});

test('Should be StorageResolution `1` if MetricResolution is set to `High`',()=>{
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.High);
const serializedMetrics = metrics.serializeMetrics();

expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High);
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1);
});

test('Should be StorageResolution `1` if MetricResolution is set to `1`',()=>{
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10, 1);
const serializedMetrics = metrics.serializeMetrics();

expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High);
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1);

});
});

describe('Feature: Clearing Metrics ', () => {
test('Clearing metrics should return empty', async () => {
const metrics = new Metrics({ namespace: 'test' });
Expand Down
Loading

0 comments on commit 79a321b

Please sign in to comment.