Skip to content

feat(metrics): support high resolution metrics #1369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
29 changes: 24 additions & 5 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ExtraOptions,
MetricUnit,
MetricUnits,
MetricResolution,
} from './types';

const MAX_METRICS_SIZE = 100;
Expand Down Expand Up @@ -168,9 +169,11 @@ class Metrics extends Utility implements MetricsInterface {
* @param name
* @param unit
* @param value
* @param resolution
*/
public addMetric(name: string, unit: MetricUnit, value: number): void {
this.storeMetric(name, unit, value);

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

Expand Down Expand Up @@ -319,10 +322,15 @@ class Metrics extends Utility implements MetricsInterface {
* @returns {string}
*/
public serializeMetrics(): EmfOutput {
const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({
const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => 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 @@ -479,7 +487,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 @@ -489,7 +502,9 @@ class Metrics extends Utility implements MetricsInterface {
unit,
value,
name,
resolution,
};

} else {
const storedMetric = this.storedMetrics[name];
if (!Array.isArray(storedMetric.value)) {
Expand All @@ -501,4 +516,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 };
10 changes: 8 additions & 2 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,12 @@ type EmfOutput = {
Timestamp: number
CloudWatchMetrics: {
Namespace: string
Dimensions: [string[]]
Metrics: { Name: string; Unit: MetricUnit }[]
Dimensions: [string[]]
Metrics: {
Name: string
Unit: MetricUnit
StorageResolution?: MetricResolution
}[]
}[]
}
};
Expand Down Expand Up @@ -60,6 +65,7 @@ type StoredMetric = {
name: string
unit: MetricUnit
value: number | number[]
resolution?: MetricResolution
};

type StoredMetrics = {
Expand Down
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';
93 changes: 92 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,56 @@ 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('should set StorageResolution 60 if MetricResolution is set to `Standard`', () => {
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);
});

test('Should be StorageResolution 60 if MetricResolution is set to `60`',()=>{
const metrics = new Metrics();
metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60);
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);
});

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 Expand Up @@ -747,4 +798,44 @@ describe('Class: Metrics', () => {
expect(loggedData._aws.CloudWatchMetrics[0].Namespace).toEqual(namespace);
});
});

describe('concept', ()=>{
test('metric DX type with const', ()=>{
const MetricResolutionConcept = {
Standard: 60,
High: 1
} as const;
type MetricResolutionConcept = typeof MetricResolutionConcept[keyof typeof MetricResolutionConcept];

const use = (resolution: MetricResolutionConcept):void => {
if (resolution === MetricResolutionConcept.Standard) expect(resolution).toBe(MetricResolution.Standard);
if (resolution === MetricResolution.High) expect(resolution).toBe(MetricResolutionConcept.High);
};

// prefered design of Metric Resolution, strcutural typing, compile time guidance
use(MetricResolution.Standard);
use(60);
use(1);
// use(10); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345)
//use(80); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345)
});

test('metric DX type with enum', ()=>{
enum MetricResolutionEnum {
Standard = 60,
High = 1
}

const use = (resolution: MetricResolutionEnum):void => {
if (resolution === MetricResolutionEnum.Standard) expect(resolution).toBe(MetricResolution.Standard);
if (resolution === MetricResolutionEnum.High) expect(resolution).toBe(MetricResolutionEnum.High);
};
use(MetricResolution.Standard);

// enum design, allows the following usage at compile time
use(10); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum'
use(80); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum'
});

});
});
82 changes: 80 additions & 2 deletions packages/metrics/tests/unit/middleware/middy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
* @group unit/metrics/middleware
*/

import { Metrics, MetricUnits, logMetrics } from '../../../../metrics/src';
import middy from '@middy/core';
import {
Metrics,
MetricUnits,
logMetrics,
MetricResolution
} from '../../../../metrics/src';import middy from '@middy/core';
import { ExtraOptions } from '../../../src/types';

const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
Expand Down Expand Up @@ -315,4 +319,78 @@ describe('Middy middleware', () => {
);
});
});
describe('Metrics resolution', () => {

test('should use metric resolution `Standard, 60` if `Standard` is set', async () => {
// Prepare
const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });

const lambdaHandler = (): void => {
metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.Standard);
};

const handler = middy(lambdaHandler).use(logMetrics(metrics));

// Act
await handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));

// Assess
expect(console.log).toHaveBeenCalledWith(
JSON.stringify({
_aws: {
Timestamp: 1466424490000,
CloudWatchMetrics: [
{
Namespace: 'serverlessAirline',
Dimensions: [['service']],
Metrics: [{
Name: 'successfulBooking',
Unit: 'Count',
StorageResolution: 60,
}],
},
],
},
service: 'orders',
successfulBooking: 1,
})
);
});

test('should use metric resolution `High, 1` if `High` is set', async () => {
// Prepare
const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });

const lambdaHandler = (): void => {
metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High);
};

const handler = middy(lambdaHandler).use(logMetrics(metrics));

// Act
await handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!'));

// Assess
expect(console.log).toHaveBeenCalledWith(
JSON.stringify({
_aws: {
Timestamp: 1466424490000,
CloudWatchMetrics: [
{
Namespace: 'serverlessAirline',
Dimensions: [['service']],
Metrics: [{
Name: 'successfulBooking',
Unit: 'Count',
StorageResolution: 1
}],
},
],
},
service: 'orders',
successfulBooking: 1,
})
);
});
});
});