Skip to content

Commit 59e96a3

Browse files
authored
fix(cloudwatch): period of each metric in usingMetrics for MathExpression is ignored (#30986)
### Issue # (if applicable) Closes #<issue number here>. ### Reason for this change The `usingMetrics` property (`Record<string, IMetric>`) in `MathExpressionProps` has Metric objects with a `period`. On the other hand, in the `MathExpression` construct, the `period` of each metric in the `usingMetrics` is ignored and instead overridden by the `period` of the `MathExpression`. Even if the `period` of the `MathExpression` is not specified, it is overridden by its default value. https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts#L606-L608 However the statement is not written in the JSDoc. https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts#L566 ```ts new MathExpression({ expression: "m1+m2", label: "AlbErrors", usingMetrics: { m1: metrics.custom("HTTPCode_ELB_500_Count", { period: Duration.minutes(1), // <- ignored and overridden by default value `Duration.minutes(5)` of `period` in the `MathExpressionOptions` statistic: "Sum", label: "HTTPCode_ELB_500_Count", }), m2: metrics.custom("HTTPCode_ELB_502_Count", { period: Duration.minutes(1), // <- ignored and overridden by default value `Duration.minutes(5)` of `period` in the `MathExpressionOptions` statistic: "Sum", label: "HTTPCode_ELB_502_Count", }), }, }), ``` ### Description of changes The current documentation could be confusing to users, so add this description. Also added warnings in the situation. ### Description of how you validated changes ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4937ee1 commit 59e96a3

File tree

3 files changed

+148
-10
lines changed

3 files changed

+148
-10
lines changed

packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts

+63-10
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,35 @@ export interface MathExpressionProps extends MathExpressionOptions {
226226
* The key is the identifier that represents the given metric in the
227227
* expression, and the value is the actual Metric object.
228228
*
229+
* The `period` of each metric in `usingMetrics` is ignored and instead overridden
230+
* by the `period` specified for the `MathExpression` construct. Even if no `period`
231+
* is specified for the `MathExpression`, it will be overridden by the default
232+
* value (`Duration.minutes(5)`).
233+
*
234+
* Example:
235+
*
236+
* ```ts
237+
* declare const metrics: elbv2.IApplicationLoadBalancerMetrics;
238+
* new cloudwatch.MathExpression({
239+
* expression: 'm1+m2',
240+
* label: 'AlbErrors',
241+
* usingMetrics: {
242+
* m1: metrics.custom('HTTPCode_ELB_500_Count', {
243+
* period: Duration.minutes(1), // <- This period will be ignored
244+
* statistic: 'Sum',
245+
* label: 'HTTPCode_ELB_500_Count',
246+
* }),
247+
* m2: metrics.custom('HTTPCode_ELB_502_Count', {
248+
* period: Duration.minutes(1), // <- This period will be ignored
249+
* statistic: 'Sum',
250+
* label: 'HTTPCode_ELB_502_Count',
251+
* }),
252+
* },
253+
* period: Duration.minutes(3), // <- This overrides the period of each metric in `usingMetrics`
254+
* // (Even if not specified, it is overridden by the default value)
255+
* });
256+
* ```
257+
*
229258
* @default - Empty map.
230259
*/
231260
readonly usingMetrics?: Record<string, IMetric>;
@@ -605,12 +634,19 @@ export class MathExpression implements IMetric {
605634
constructor(props: MathExpressionProps) {
606635
this.period = props.period || cdk.Duration.minutes(5);
607636
this.expression = props.expression;
608-
this.usingMetrics = changeAllPeriods(props.usingMetrics ?? {}, this.period);
609637
this.label = props.label;
610638
this.color = props.color;
611639
this.searchAccount = props.searchAccount;
612640
this.searchRegion = props.searchRegion;
613641

642+
const { record, overridden } = changeAllPeriods(props.usingMetrics ?? {}, this.period);
643+
this.usingMetrics = record;
644+
645+
const warnings: { [id: string]: string } = {};
646+
if (overridden) {
647+
warnings['CloudWatch:Math:MetricsPeriodsOverridden'] = `Periods of metrics in 'usingMetrics' for Math expression '${this.expression}' have been overridden to ${this.period.toSeconds()} seconds.`;
648+
}
649+
614650
const invalidVariableNames = Object.keys(this.usingMetrics).filter(x => !validVariableName(x));
615651
if (invalidVariableNames.length > 0) {
616652
throw new Error(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`);
@@ -624,7 +660,6 @@ export class MathExpression implements IMetric {
624660
// we can add warnings.
625661
const missingIdentifiers = allIdentifiersInExpression(this.expression).filter(i => !this.usingMetrics[i]);
626662

627-
const warnings: { [id: string]: string } = {};
628663
if (!this.expression.toUpperCase().match('\\s*INSIGHT_RULE_METRIC|SELECT|SEARCH|METRICS\\s.*') && missingIdentifiers.length > 0) {
629664
warnings['CloudWatch:Math:UnknownIdentifier'] = `Math expression '${this.expression}' references unknown identifiers: ${missingIdentifiers.join(', ')}. Please add them to the 'usingMetrics' map.`;
630665
}
@@ -879,23 +914,33 @@ function ifUndefined<T>(x: T | undefined, def: T | undefined): T | undefined {
879914
/**
880915
* Change periods of all metrics in the map
881916
*/
882-
function changeAllPeriods(metrics: Record<string, IMetric>, period: cdk.Duration): Record<string, IMetric> {
883-
const ret: Record<string, IMetric> = {};
884-
for (const [id, metric] of Object.entries(metrics)) {
885-
ret[id] = changePeriod(metric, period);
917+
function changeAllPeriods(metrics: Record<string, IMetric>, period: cdk.Duration): { record: Record<string, IMetric>; overridden: boolean } {
918+
const retRecord: Record<string, IMetric> = {};
919+
let retOverridden = false;
920+
for (const [id, m] of Object.entries(metrics)) {
921+
const { metric, overridden } = changePeriod(m, period);
922+
retRecord[id] = metric;
923+
if (overridden) {
924+
retOverridden = true;
925+
}
886926
}
887-
return ret;
927+
return { record: retRecord, overridden: retOverridden };
888928
}
889929

890930
/**
891-
* Return a new metric object which is the same type as the input object, but with the period changed
931+
* Return a new metric object which is the same type as the input object but with the period changed,
932+
* and a flag to indicate whether the period has been overwritten.
892933
*
893934
* Relies on the fact that implementations of `IMetric` are also supposed to have
894935
* an implementation of `with` that accepts an argument called `period`. See `IModifiableMetric`.
895936
*/
896-
function changePeriod(metric: IMetric, period: cdk.Duration): IMetric {
937+
function changePeriod(metric: IMetric, period: cdk.Duration): { metric: IMetric; overridden: boolean} {
897938
if (isModifiableMetric(metric)) {
898-
return metric.with({ period });
939+
const overridden =
940+
isMetricWithPeriod(metric) && // always true, as the period property is set with a default value even if it is not specified
941+
metric.period.toSeconds() !== cdk.Duration.minutes(5).toSeconds() && // exclude the default value of a metric, assuming the user has not specified it
942+
metric.period.toSeconds() !== period.toSeconds();
943+
return { metric: metric.with({ period }), overridden };
899944
}
900945

901946
throw new Error(`Metric object should also implement 'with': ${metric}`);
@@ -927,6 +972,14 @@ function isModifiableMetric(m: any): m is IModifiableMetric {
927972
return typeof m === 'object' && m !== null && !!m.with;
928973
}
929974

975+
interface IMetricWithPeriod {
976+
period: cdk.Duration;
977+
}
978+
979+
function isMetricWithPeriod(m: any): m is IMetricWithPeriod {
980+
return typeof m === 'object' && m !== null && !!m.period;
981+
}
982+
930983
// Polyfill for string.matchAll(regexp)
931984
function matchAll(x: string, re: RegExp): RegExpMatchArray[] {
932985
const ret = new Array<RegExpMatchArray>();

packages/aws-cdk-lib/aws-cloudwatch/test/metric-math.test.ts

+84
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,90 @@ describe('Metric Math', () => {
124124
});
125125
});
126126

127+
test('warn if a period is specified in usingMetrics and not equal to the value of the period for MathExpression', () => {
128+
const m = new MathExpression({
129+
expression: 'm1',
130+
usingMetrics: {
131+
m1: new Metric({ namespace: 'Test', metricName: 'ACount', period: Duration.minutes(4) }),
132+
},
133+
period: Duration.minutes(3),
134+
});
135+
136+
expect(m.warningsV2).toMatchObject({
137+
'CloudWatch:Math:MetricsPeriodsOverridden': 'Periods of metrics in \'usingMetrics\' for Math expression \'m1\' have been overridden to 180 seconds.',
138+
});
139+
});
140+
141+
test('warn if periods are specified in usingMetrics and one is not equal to the value of the period for MathExpression', () => {
142+
const m = new MathExpression({
143+
expression: 'm1 + m2',
144+
usingMetrics: {
145+
m1: new Metric({ namespace: 'Test', metricName: 'ACount', period: Duration.minutes(4) }),
146+
m2: new Metric({ namespace: 'Test', metricName: 'BCount', period: Duration.minutes(3) }),
147+
},
148+
period: Duration.minutes(3),
149+
});
150+
151+
expect(m.warningsV2).toMatchObject({
152+
'CloudWatch:Math:MetricsPeriodsOverridden': 'Periods of metrics in \'usingMetrics\' for Math expression \'m1 + m2\' have been overridden to 180 seconds.',
153+
});
154+
});
155+
156+
test('warn if a period is specified in usingMetrics and not equal to the default value of the period for MathExpression', () => {
157+
const m = new MathExpression({
158+
expression: 'm1',
159+
usingMetrics: {
160+
m1: new Metric({ namespace: 'Test', metricName: 'ACount', period: Duration.minutes(4) }),
161+
},
162+
});
163+
164+
expect(m.warningsV2).toMatchObject({
165+
'CloudWatch:Math:MetricsPeriodsOverridden': 'Periods of metrics in \'usingMetrics\' for Math expression \'m1\' have been overridden to 300 seconds.',
166+
});
167+
});
168+
169+
test('can raise multiple warnings', () => {
170+
const m = new MathExpression({
171+
expression: 'e1 + m1',
172+
usingMetrics: {
173+
e1: new MathExpression({
174+
expression: 'n1 + n2',
175+
}),
176+
m1: new Metric({ namespace: 'Test', metricName: 'ACount', period: Duration.minutes(4) }),
177+
},
178+
period: Duration.minutes(3),
179+
});
180+
181+
expect(m.warningsV2).toMatchObject({
182+
'CloudWatch:Math:MetricsPeriodsOverridden': 'Periods of metrics in \'usingMetrics\' for Math expression \'e1 + m1\' have been overridden to 180 seconds.',
183+
'CloudWatch:Math:UnknownIdentifier': expect.stringContaining("'n1 + n2' references unknown identifiers"),
184+
});
185+
});
186+
187+
test('don\'t warn if a period is not specified in usingMetrics', () => {
188+
const m = new MathExpression({
189+
expression: 'm1',
190+
usingMetrics: {
191+
m1: new Metric({ namespace: 'Test', metricName: 'ACount' }),
192+
},
193+
period: Duration.minutes(3),
194+
});
195+
196+
expect(m.warningsV2).toBeUndefined();
197+
});
198+
199+
test('don\'t warn if a period is specified in usingMetrics but equal to the value of the period for MathExpression', () => {
200+
const m = new MathExpression({
201+
expression: 'm1',
202+
usingMetrics: {
203+
m1: new Metric({ namespace: 'Test', metricName: 'ACount', period: Duration.minutes(3) }),
204+
},
205+
period: Duration.minutes(3),
206+
});
207+
208+
expect(m.warningsV2).toBeUndefined();
209+
});
210+
127211
describe('in graphs', () => {
128212
test('MathExpressions can be added to a graph', () => {
129213
// GIVEN

packages/aws-cdk-lib/rosetta/aws_cloudwatch/default.ts-fixture

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Construct } from 'constructs';
33
import { Stack, Duration } from 'aws-cdk-lib';
44
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
5+
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
56
import * as route53 from 'aws-cdk-lib/aws-route53';
67
import * as sns from 'aws-cdk-lib/aws-sns';
78
import * as lambda from 'aws-cdk-lib/aws-lambda';

0 commit comments

Comments
 (0)