Skip to content

Commit 79a321b

Browse files
feat(metrics): support high resolution metrics (#1369)
1 parent 8a47e76 commit 79a321b

File tree

10 files changed

+261
-26
lines changed

10 files changed

+261
-26
lines changed

Diff for: docs/core/metrics.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar
2727

2828
* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
2929
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
30+
* **Metric**. It's the name of the metric, for example: SuccessfulBooking or UpdatedBooking.
31+
* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: Count or Seconds.
32+
* **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).
3033

3134
<figure>
3235
<img src="../../media/metrics_terminology.png" />
@@ -117,7 +120,23 @@ You can create metrics using the `addMetric` method, and you can create dimensio
117120
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.
118121

119122
!!! warning "Do not create metrics or dimensions outside the handler"
120-
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behaviour.
123+
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.
124+
125+
### Adding high-resolution metrics
126+
127+
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`.
128+
129+
!!! tip "When is it useful?"
130+
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.
131+
132+
=== "Metrics with high resolution"
133+
134+
```typescript hl_lines="6"
135+
--8<-- "docs/snippets/metrics/addHighResolutionMetric.ts"
136+
```
137+
138+
!!! tip "Autocomplete Metric Resolutions"
139+
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.
121140

122141
### Adding multi-value metrics
123142

Diff for: docs/snippets/metrics/addHighResolutionMetric.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Metrics, MetricUnits, MetricResolution } from '@aws-lambda-powertools/metrics';
2+
3+
const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });
4+
5+
export const handler = async (_event: unknown, _context: unknown): Promise<void> => {
6+
metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High);
7+
};

Diff for: docs/snippets/metrics/basicUsage.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
22

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

5-
export const handler = async (_event, _context): Promise<void> => {
5+
export const handler = async (_event: unknown, _context: unknown): Promise<void> => {
66
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
77
};

Diff for: packages/metrics/src/Metrics.ts

+67-14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
ExtraOptions,
1212
MetricUnit,
1313
MetricUnits,
14+
MetricResolution,
15+
MetricDefinition,
16+
StoredMetric,
1417
} from './types';
1518

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

166169
/**
167170
* Add a metric to the metrics buffer.
168-
* @param name
169-
* @param unit
170-
* @param value
171+
*
172+
* @example
173+
*
174+
* Add Metric using MetricUnit Enum supported by Cloudwatch
175+
*
176+
* ```ts
177+
* metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
178+
* ```
179+
*
180+
* @example
181+
*
182+
* Add Metric using MetricResolution type with resolutions High or Standard supported by cloudwatch
183+
*
184+
* ```ts
185+
* metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High);
186+
* ```
187+
*
188+
* @param name - The metric name
189+
* @param unit - The metric unit
190+
* @param value - The metric value
191+
* @param resolution - The metric resolution
192+
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition Amazon Cloudwatch Concepts Documentation
193+
* @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
171194
*/
172-
public addMetric(name: string, unit: MetricUnit, value: number): void {
173-
this.storeMetric(name, unit, value);
195+
196+
public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = MetricResolution.Standard): void {
197+
this.storeMetric(name, unit, value, resolution);
174198
if (this.isSingleMetric) this.publishStoredMetrics();
175199
}
176200

@@ -314,15 +338,29 @@ class Metrics extends Utility implements MetricsInterface {
314338
}
315339

316340
/**
317-
* Function to create the right object compliant with Cloudwatch EMF (Event Metric Format).
341+
* Function to create the right object compliant with Cloudwatch EMF (Embedded Metric Format).
342+
*
343+
*
344+
* @returns metrics as JSON object compliant EMF Schema Specification
318345
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html for more details
319-
* @returns {string}
320346
*/
321347
public serializeMetrics(): EmfOutput {
322-
const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({
323-
Name: metricDefinition.name,
324-
Unit: metricDefinition.unit,
325-
}));
348+
// For high-resolution metrics, add StorageResolution property
349+
// Example: [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 1 } ]
350+
351+
// For standard resolution metrics, don't add StorageResolution property to avoid unnecessary ingestion of data into cloudwatch
352+
// Example: [ { "Name": "metric_name", "Unit": "Count"} ]
353+
const metricDefinitions: MetricDefinition[] = Object.values(this.storedMetrics).map((metricDefinition) =>
354+
this.isHigh(metricDefinition['resolution'])
355+
? ({
356+
Name: metricDefinition.name,
357+
Unit: metricDefinition.unit,
358+
StorageResolution: metricDefinition.resolution
359+
}): ({
360+
Name: metricDefinition.name,
361+
Unit: metricDefinition.unit,
362+
}));
363+
326364
if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) {
327365
throw new RangeError('The number of metrics recorded must be higher than zero');
328366
}
@@ -429,6 +467,10 @@ class Metrics extends Utility implements MetricsInterface {
429467
return <EnvironmentVariablesService> this.envVarsService;
430468
}
431469

470+
private isHigh(resolution: StoredMetric['resolution']): resolution is typeof MetricResolution['High'] {
471+
return resolution === MetricResolution.High;
472+
}
473+
432474
private isNewMetric(name: string, unit: MetricUnit): boolean {
433475
if (this.storedMetrics[name]){
434476
// Inconsistent units indicates a bug or typos and we want to flag this to users early
@@ -479,7 +521,12 @@ class Metrics extends Utility implements MetricsInterface {
479521
}
480522
}
481523

482-
private storeMetric(name: string, unit: MetricUnit, value: number): void {
524+
private storeMetric(
525+
name: string,
526+
unit: MetricUnit,
527+
value: number,
528+
resolution: MetricResolution,
529+
): void {
483530
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
484531
this.publishStoredMetrics();
485532
}
@@ -488,8 +535,10 @@ class Metrics extends Utility implements MetricsInterface {
488535
this.storedMetrics[name] = {
489536
unit,
490537
value,
491-
name,
538+
name,
539+
resolution
492540
};
541+
493542
} else {
494543
const storedMetric = this.storedMetrics[name];
495544
if (!Array.isArray(storedMetric.value)) {
@@ -501,4 +550,8 @@ class Metrics extends Utility implements MetricsInterface {
501550

502551
}
503552

504-
export { Metrics, MetricUnits };
553+
export {
554+
Metrics,
555+
MetricUnits,
556+
MetricResolution,
557+
};

Diff for: packages/metrics/src/MetricsInterface.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { Metrics } from './Metrics';
2-
import { MetricUnit, EmfOutput, HandlerMethodDecorator, Dimensions, MetricsOptions } from './types';
3-
2+
import {
3+
MetricUnit,
4+
MetricResolution,
5+
EmfOutput,
6+
HandlerMethodDecorator,
7+
Dimensions,
8+
MetricsOptions
9+
} from './types';
410
interface MetricsInterface {
511
addDimension(name: string, value: string): void
612
addDimensions(dimensions: {[key: string]: string}): void
713
addMetadata(key: string, value: string): void
8-
addMetric(name: string, unit:MetricUnit, value:number): void
14+
addMetric(name: string, unit:MetricUnit, value:number, resolution?: MetricResolution): void
915
clearDimensions(): void
1016
clearMetadata(): void
1117
clearMetrics(): void

Diff for: packages/metrics/src/types/MetricResolution.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const MetricResolution = {
2+
Standard: 60,
3+
High: 1,
4+
} as const;
5+
6+
type MetricResolution = typeof MetricResolution[keyof typeof MetricResolution];
7+
8+
export { MetricResolution };

Diff for: packages/metrics/src/types/Metrics.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Handler } from 'aws-lambda';
22
import { LambdaInterface, AsyncHandler, SyncHandler } from '@aws-lambda-powertools/commons';
33
import { ConfigServiceInterface } from '../config';
44
import { MetricUnit } from './MetricUnit';
5+
import { MetricResolution } from './MetricResolution';
56

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

@@ -19,8 +20,8 @@ type EmfOutput = {
1920
Timestamp: number
2021
CloudWatchMetrics: {
2122
Namespace: string
22-
Dimensions: [string[]]
23-
Metrics: { Name: string; Unit: MetricUnit }[]
23+
Dimensions: [string[]]
24+
Metrics: MetricDefinition[]
2425
}[]
2526
}
2627
};
@@ -60,10 +61,17 @@ type StoredMetric = {
6061
name: string
6162
unit: MetricUnit
6263
value: number | number[]
64+
resolution: MetricResolution
6365
};
6466

6567
type StoredMetrics = {
6668
[key: string]: StoredMetric
6769
};
6870

69-
export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics };
71+
type MetricDefinition = {
72+
Name: string
73+
Unit: MetricUnit
74+
StorageResolution?: MetricResolution
75+
};
76+
77+
export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics, StoredMetric, MetricDefinition };

Diff for: packages/metrics/src/types/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './Metrics';
2-
export * from './MetricUnit';
2+
export * from './MetricUnit';
3+
export * from './MetricResolution';

Diff for: packages/metrics/tests/unit/Metrics.test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import { ContextExamples as dummyContext, Events as dummyEvent, LambdaInterface } from '@aws-lambda-powertools/commons';
88
import { Context, Callback } from 'aws-lambda';
9-
import { Metrics, MetricUnits } from '../../src/';
9+
10+
import { Metrics, MetricUnits, MetricResolution } from '../../src/';
1011

1112
const MAX_METRICS_SIZE = 100;
1213
const MAX_DIMENSION_COUNT = 29;
@@ -563,6 +564,61 @@ describe('Class: Metrics', () => {
563564
});
564565
});
565566

567+
describe('Feature: Resolution of Metrics', ()=>{
568+
569+
test('serialized metrics in EMF format should not contain `StorageResolution` as key if none is set', () => {
570+
const metrics = new Metrics();
571+
metrics.addMetric('test_name', MetricUnits.Seconds, 10);
572+
const serializedMetrics = metrics.serializeMetrics();
573+
574+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
575+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
576+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');
577+
578+
});
579+
test('serialized metrics in EMF format should not contain `StorageResolution` as key if `Standard` is set', () => {
580+
const metrics = new Metrics();
581+
metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.Standard);
582+
const serializedMetrics = metrics.serializeMetrics();
583+
584+
// expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.Standard);
585+
// expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(60);
586+
587+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
588+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
589+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');
590+
});
591+
592+
test('serialized metrics in EMF format should not contain `StorageResolution` as key if `60` is set',()=>{
593+
const metrics = new Metrics();
594+
metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60);
595+
const serializedMetrics = metrics.serializeMetrics();
596+
597+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution');
598+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name');
599+
expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit');
600+
});
601+
602+
test('Should be StorageResolution `1` if MetricResolution is set to `High`',()=>{
603+
const metrics = new Metrics();
604+
metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.High);
605+
const serializedMetrics = metrics.serializeMetrics();
606+
607+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High);
608+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1);
609+
});
610+
611+
test('Should be StorageResolution `1` if MetricResolution is set to `1`',()=>{
612+
const metrics = new Metrics();
613+
metrics.addMetric('test_name', MetricUnits.Seconds, 10, 1);
614+
const serializedMetrics = metrics.serializeMetrics();
615+
616+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High);
617+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1);
618+
619+
});
620+
});
621+
566622
describe('Feature: Clearing Metrics ', () => {
567623
test('Clearing metrics should return empty', async () => {
568624
const metrics = new Metrics({ namespace: 'test' });

0 commit comments

Comments
 (0)