Skip to content

Commit 91a2bba

Browse files
ijemmyheitorlessasaragerion
authored
fix(metrics): Support multiple addMetric() call with the same metric name (#390)
* fix(metrics): Support multiple addMetric() call with the same metric name * docs(metrics): Add section about adding the same metric name multiple time * docs(metrics): Rearrange excerpt example to be the same as others * docs(metrics) Change to more appropriate term Co-authored-by: Heitor Lessa <[email protected]> * fix(metrics): Fix typos + grammar in a comment Co-authored-by: Sara Gerion <[email protected]> * fix(metrics): change an error message to suggest a potential solution Co-authored-by: Heitor Lessa <[email protected]> Co-authored-by: Sara Gerion <[email protected]>
1 parent 82f9edc commit 91a2bba

File tree

7 files changed

+206
-54
lines changed

7 files changed

+206
-54
lines changed

docs/core/metrics.md

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,54 @@ You can create metrics using `addMetric`, and you can create dimensions for all
132132
!!! warning "Do not create metrics or dimensions outside the handler"
133133
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behaviour.
134134

135+
### Adding multi-value metrics
136+
You can call `addMetric()` with the same name multiple times. The values will be grouped together in an array.
137+
138+
=== "addMetric() with the same name"
139+
140+
```typescript hl_lines="8 10"
141+
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
142+
import { Context } from 'aws-lambda';
143+
144+
145+
const metrics = new Metrics({namespace:"serverlessAirline", service:"orders"});
146+
147+
export const handler = async (event: any, context: Context) => {
148+
metrics.addMetric('performedActionA', MetricUnits.Count, 2);
149+
// do something else...
150+
metrics.addMetric('performedActionA', MetricUnits.Count, 1);
151+
}
152+
```
153+
=== "Example CloudWatch Logs excerpt"
154+
155+
```json hl_lines="2-5 18-19"
156+
{
157+
"performedActionA": [
158+
2,
159+
1
160+
],
161+
"_aws": {
162+
"Timestamp": 1592234975665,
163+
"CloudWatchMetrics": [
164+
{
165+
"Namespace": "serverlessAirline",
166+
"Dimensions": [
167+
[
168+
"service"
169+
]
170+
],
171+
"Metrics": [
172+
{
173+
"Name": "performedActionA",
174+
"Unit": "Count"
175+
}
176+
]
177+
}
178+
]
179+
},
180+
"service": "orders"
181+
}
182+
```
135183
### Adding default dimensions
136184

137185
You can use add default dimensions to your metrics by passing them as parameters in 4 ways:
@@ -264,23 +312,23 @@ See below an example of how to automatically flush metrics with the Middy-compat
264312
{
265313
"bookingConfirmation": 1.0,
266314
"_aws": {
267-
"Timestamp": 1592234975665,
268-
"CloudWatchMetrics": [
269-
{
270-
"Namespace": "exampleApplication",
271-
"Dimensions": [
272-
[
273-
"service"
274-
]
275-
],
276-
"Metrics": [
315+
"Timestamp": 1592234975665,
316+
"CloudWatchMetrics": [
277317
{
278-
"Name": "bookingConfirmation",
279-
"Unit": "Count"
318+
"Namespace": "exampleApplication",
319+
"Dimensions": [
320+
[
321+
"service"
322+
]
323+
],
324+
"Metrics": [
325+
{
326+
"Name": "bookingConfirmation",
327+
"Unit": "Count"
328+
}
329+
]
280330
}
281331
]
282-
}
283-
]
284332
},
285333
"service": "exampleService"
286334
}
@@ -316,23 +364,23 @@ export class MyFunction {
316364
{
317365
"bookingConfirmation": 1.0,
318366
"_aws": {
319-
"Timestamp": 1592234975665,
320-
"CloudWatchMetrics": [
321-
{
322-
"Namespace": "exampleApplication",
323-
"Dimensions": [
324-
[
325-
"service"
326-
]
327-
],
328-
"Metrics": [
367+
"Timestamp": 1592234975665,
368+
"CloudWatchMetrics": [
329369
{
330-
"Name": "bookingConfirmation",
331-
"Unit": "Count"
370+
"Namespace": "exampleApplication",
371+
"Dimensions": [
372+
[
373+
"service"
374+
]
375+
],
376+
"Metrics": [
377+
{
378+
"Name": "bookingConfirmation",
379+
"Unit": "Count"
380+
}
381+
]
332382
}
333383
]
334-
}
335-
]
336384
},
337385
"service": "exampleService"
338386
}
@@ -456,23 +504,23 @@ You can add high-cardinality data as part of your Metrics log with `addMetadata`
456504
{
457505
"successfulBooking": 1.0,
458506
"_aws": {
459-
"Timestamp": 1592234975665,
460-
"CloudWatchMetrics": [
461-
{
462-
"Namespace": "exampleApplication",
463-
"Dimensions": [
464-
[
465-
"service"
466-
]
467-
],
468-
"Metrics": [
507+
"Timestamp": 1592234975665,
508+
"CloudWatchMetrics": [
469509
{
470-
"Name": "successfulBooking",
471-
"Unit": "Count"
510+
"Namespace": "exampleApplication",
511+
"Dimensions": [
512+
[
513+
"service"
514+
]
515+
],
516+
"Metrics": [
517+
{
518+
"Name": "successfulBooking",
519+
"Unit": "Count"
520+
}
521+
]
472522
}
473523
]
474-
}
475-
]
476524
},
477525
"service": "booking",
478526
"bookingId": "7051cd10-6283-11ec-90d6-0242ac120003"

packages/metrics/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/metrics/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"commit": "commit",
1111
"test": "jest --group=unit --detectOpenHandles --coverage --verbose",
1212
"test:e2e": "jest --group=e2e",
13-
"watch": "jest --watch",
13+
"watch": "jest --group=unit --watch ",
1414
"build": "tsc",
1515
"lint": "eslint --ext .ts --fix --no-error-on-unmatched-pattern src tests",
1616
"format": "eslint --fix --ext .ts --fix --no-error-on-unmatched-pattern src tests",

packages/metrics/src/Metrics.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ class Metrics implements MetricsInterface {
312312
if (!this.namespace) console.warn('Namespace should be defined, default used');
313313

314314
const metricValues = Object.values(this.storedMetrics).reduce(
315-
(result: { [key: string]: number }, { name, value }: { name: string; value: number }) => {
315+
(result: { [key: string]: number | number[] }, { name, value }: { name: string; value: number | number[] }) => {
316316
result[name] = value;
317317

318318
return result;
@@ -391,6 +391,20 @@ class Metrics implements MetricsInterface {
391391
return <EnvironmentVariablesService> this.envVarsService;
392392
}
393393

394+
private isNewMetric(name: string, unit: MetricUnit): boolean {
395+
if (this.storedMetrics[name]){
396+
// Inconsistent units indicates a bug or typos and we want to flag this to users early
397+
if (this.storedMetrics[name].unit !== unit) {
398+
const currentUnit = this.storedMetrics[name].unit;
399+
throw new Error(`Metric "${name}" has already been added with unit "${currentUnit}", but we received unit "${unit}". Did you mean to use metric unit "${currentUnit}"?`);
400+
}
401+
402+
return false;
403+
} else {
404+
return true;
405+
}
406+
}
407+
394408
private setCustomConfigService(customConfigService?: ConfigServiceInterface): void {
395409
this.customConfigService = customConfigService ? customConfigService : undefined;
396410
}
@@ -431,12 +445,22 @@ class Metrics implements MetricsInterface {
431445
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
432446
this.publishStoredMetrics();
433447
}
434-
this.storedMetrics[name] = {
435-
unit,
436-
value,
437-
name,
438-
};
448+
449+
if (this.isNewMetric(name, unit)) {
450+
this.storedMetrics[name] = {
451+
unit,
452+
value,
453+
name,
454+
};
455+
} else {
456+
const storedMetric = this.storedMetrics[name];
457+
if (!Array.isArray(storedMetric.value)) {
458+
storedMetric.value = [storedMetric.value];
459+
}
460+
storedMetric.value.push(value);
461+
}
439462
}
463+
440464
}
441465

442466
export { Metrics, MetricUnits };

packages/metrics/src/types/Metrics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type ExtraOptions = {
5959
type StoredMetric = {
6060
name: string
6161
unit: MetricUnit
62-
value: number
62+
value: number | number[]
6363
};
6464

6565
type StoredMetrics = {

packages/metrics/tests/unit/Metrics.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,55 @@ describe('Class: Metrics', () => {
435435
expect(serializedMetrics._aws.CloudWatchMetrics[0].Namespace).toBe(DEFAULT_NAMESPACE);
436436
expect(console.warn).toHaveBeenNthCalledWith(1, 'Namespace should be defined, default used');
437437
});
438+
439+
test('Should contain a metric value if added once', ()=> {
440+
const metrics = new Metrics();
441+
442+
metrics.addMetric('test_name', MetricUnits.Count, 1);
443+
const serializedMetrics = metrics.serializeMetrics();
444+
445+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics.length).toBe(1);
446+
447+
expect(serializedMetrics['test_name']).toBe(1);
448+
});
449+
450+
test('Should convert multiple metrics with the same name and unit into an array', ()=> {
451+
const metrics = new Metrics();
452+
453+
metrics.addMetric('test_name', MetricUnits.Count, 2);
454+
metrics.addMetric('test_name', MetricUnits.Count, 1);
455+
const serializedMetrics = metrics.serializeMetrics();
456+
457+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics.length).toBe(1);
458+
expect(serializedMetrics['test_name']).toStrictEqual([ 2, 1 ]);
459+
});
460+
461+
test('Should throw an error if the same metric name is added again with a different unit', ()=> {
462+
const metrics = new Metrics();
463+
464+
metrics.addMetric('test_name', MetricUnits.Count, 2);
465+
try {
466+
metrics.addMetric('test_name', MetricUnits.Seconds, 10);
467+
} catch (e) {
468+
expect((<Error>e).message).toBe('Metric "test_name" has already been added with unit "Count", but we received unit "Seconds". Did you mean to use metric unit "Count"?');
469+
}
470+
});
471+
472+
test('Should contain multiple metric values if added with multiple names', ()=> {
473+
const metrics = new Metrics();
474+
475+
metrics.addMetric('test_name', MetricUnits.Count, 1);
476+
metrics.addMetric('test_name2', MetricUnits.Count, 2);
477+
const serializedMetrics = metrics.serializeMetrics();
478+
479+
expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics).toStrictEqual([
480+
{ Name: 'test_name', Unit: 'Count' },
481+
{ Name: 'test_name2', Unit: 'Count' },
482+
]);
483+
484+
expect(serializedMetrics['test_name']).toBe(1);
485+
expect(serializedMetrics['test_name2']).toBe(2);
486+
});
438487
});
439488

440489
describe('Feature: Clearing Metrics ', () => {

packages/metrics/tests/unit/middleware/middy.test.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,47 @@ describe('Middy middleware', () => {
4343
succeed: () => console.log('Succeeded!'),
4444
};
4545

46+
test('when a metrics instance receive multiple metrics with the same name, it prints multiple values in an array format', async () => {
47+
// Prepare
48+
const metrics = new Metrics({ namespace:'serverlessAirline', service:'orders' });
49+
50+
const lambdaHandler = (): void => {
51+
metrics.addMetric('successfulBooking', MetricUnits.Count, 2);
52+
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
53+
};
54+
55+
const handler = middy(lambdaHandler).use(logMetrics(metrics));
56+
57+
// Act
58+
await handler(event, context, () => console.log('Lambda invoked!'));
59+
60+
// Assess
61+
expect(console.log).toHaveBeenNthCalledWith(1, JSON.stringify({
62+
'_aws': {
63+
'Timestamp': 1466424490000,
64+
'CloudWatchMetrics': [{
65+
'Namespace': 'serverlessAirline',
66+
'Dimensions': [
67+
['service']
68+
],
69+
'Metrics': [{ 'Name': 'successfulBooking', 'Unit': 'Count' }],
70+
}],
71+
},
72+
'service': 'orders',
73+
'successfulBooking': [
74+
2,
75+
1,
76+
],
77+
}));
78+
});
79+
4680
test('when a metrics instance is passed WITH custom options, it prints the metrics in the stdout', async () => {
4781

4882
// Prepare
4983
const metrics = new Metrics({ namespace:'serverlessAirline', service:'orders' });
5084

5185
const lambdaHandler = (): void => {
5286
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
53-
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
5487
};
5588
const metricsOptions: ExtraOptions = {
5689
raiseOnEmptyMetrics: true,
@@ -106,7 +139,6 @@ describe('Middy middleware', () => {
106139

107140
const lambdaHandler = (): void => {
108141
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
109-
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
110142
};
111143

112144
const handler = middy(lambdaHandler).use(logMetrics(metrics));
@@ -139,7 +171,6 @@ describe('Middy middleware', () => {
139171

140172
const lambdaHandler = (): void => {
141173
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
142-
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
143174
};
144175
const metricsOptions: ExtraOptions = {
145176
raiseOnEmptyMetrics: true

0 commit comments

Comments
 (0)