Skip to content

Commit c393481

Browse files
fix(cloudwatch): render region and accountId when directly set on metrics (#32325)
### Issue # Closes #28731 ### Reason for this change Currently, if a user creates a metric that includes `region` and `accountId`, those fields are omitted when the metric renders in a stack with those same values. The intended behavior is to omit those fields when they're implicitly set via `metric.attachTo(stack)`, not to omit them when set directly by the user. ### Description of changes This is a second attempt at #29935, which was auto-closed after the pull request linter complained that there were no integration test changes. This time around I've tried to satisfy the linter. Otherwise, the changes are the same as before: The key changes here are on the `Metric` class. Previously, `region` and `account` were public properties that were set by `metric.attachTo(stack)`, making it impossible to differentiate whether they were set directly or via `attachTo`. To differentiate them while maintaining backward compatibility, I took this approach: 1. `attachTo` sets internal properties called `stackRegion` and `stackAccount`. Setting `region` and `account` directly sets internal properties called `regionOverride` and `accountOverride`. 2. The public `region` and `account` properties are now getters that return the override (if set) and fall back on the stack properties. 3. The `toMetricConfig()` method returns the `region` and `account` from the getters, but also includes the `regionOverride` and `accountOverride`. That way, everything that looks at `region` and `account` works the same way it did before, except in `metricGraphJson`, which skips the "if different from stack" tokenization if the overrides are set. ### Description of how you validated changes 1. Confirmed that all existing unit tests pass. 2. Added a new unit test to show that `region` and `account` are now preserved when set directly on a metric. 3. Modified an integration test to show how setting `region` and `account` on a metric affects the snapshot. ### 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 bbdd42c commit c393481

14 files changed

+154
-30
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/DashboardAndWidgetWithStartAndEnd.assets.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/DashboardAndWidgetWithStartAndEnd.template.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
{
1212
"Ref": "AWS::Region"
1313
},
14-
"\",\"metrics\":[[\"CDK/Test\",\"Metric\"]],\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":3,\"properties\":{\"view\":\"timeSeries\",\"region\":\"",
14+
"\",\"metrics\":[[\"CDK/Test\",\"Metric\",{\"accountId\":\"1234\",\"region\":\"us-north-5\"}]],\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":3,\"properties\":{\"view\":\"timeSeries\",\"region\":\"",
1515
{
1616
"Ref": "AWS::Region"
1717
},
18-
"\",\"metrics\":[[\"CDK/Test\",\"Metric\"]],\"yAxis\":{},\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":9,\"properties\":{\"view\":\"gauge\",\"region\":\"",
18+
"\",\"metrics\":[[\"CDK/Test\",\"Metric\",{\"accountId\":\"1234\",\"region\":\"us-north-5\"}]],\"yAxis\":{},\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":9,\"properties\":{\"view\":\"gauge\",\"region\":\"",
1919
{
2020
"Ref": "AWS::Region"
2121
},
22-
"\",\"metrics\":[[\"CDK/Test\",\"Metric\"]],\"yAxis\":{\"left\":{\"min\":0,\"max\":100}},\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}}]}"
22+
"\",\"metrics\":[[\"CDK/Test\",\"Metric\",{\"accountId\":\"1234\",\"region\":\"us-north-5\"}]],\"yAxis\":{\"left\":{\"min\":0,\"max\":100}},\"start\":\"-P7D\",\"end\":\"2018-12-17T06:00:00.000Z\"}}]}"
2323
]
2424
]
2525
}

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/cdk.out

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/cdkintegdashboardandwidgetwithstartandendDefaultTestDeployAssert4D8483F4.assets.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/integ.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/manifest.json

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.js.snapshot/tree.json

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.dashboard-and-widget-with-start-and-end.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class TestStack extends Stack {
1111
const testMetric = new Metric({
1212
namespace: 'CDK/Test',
1313
metricName: 'Metric',
14+
account: '1234',
15+
region: 'us-north-5',
1416
});
1517

1618
const singleValueWidget = new SingleValueWidget({

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

+14
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,20 @@ export interface MetricStatConfig {
323323
* @default Deployment account.
324324
*/
325325
readonly account?: string;
326+
327+
/**
328+
* Region set directly on the metric, not inherited from the attached stack.
329+
*
330+
* @default No override.
331+
*/
332+
readonly regionOverride?: string;
333+
334+
/**
335+
* Account set directly on the metric, not inherited from the attached stack.
336+
*
337+
* @default No override.
338+
*/
339+
readonly accountOverride?: string;
326340
}
327341

328342
/**

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

+59-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { normalizeStatistic, pairStatisticToString, parseStatistic, singleStatis
66
import { Stats } from './stats';
77
import * as iam from '../../aws-iam';
88
import * as cdk from '../../core';
9+
import { makeEnumerable } from './private/make-enumerable';
910

1011
export type DimensionHash = { [dim: string]: any };
1112

@@ -115,6 +116,20 @@ export interface CommonMetricOptions {
115116
* @default - Deployment region.
116117
*/
117118
readonly region?: string;
119+
120+
/**
121+
* Account of the stack this metric is attached to.
122+
*
123+
* @default - Deployment account.
124+
*/
125+
readonly stackAccount?: string;
126+
127+
/**
128+
* Region of the stack this metric is attached to.
129+
*
130+
* @default - Deployment region.
131+
*/
132+
readonly stackRegion?: string;
118133
}
119134

120135
/**
@@ -306,11 +321,17 @@ export class Metric implements IMetric {
306321
/** Unit of the metric. */
307322
public readonly unit?: Unit;
308323

309-
/** Account which this metric comes from */
310-
public readonly account?: string;
324+
/** Account of the stack this metric is attached to. */
325+
readonly #stackAccount?: string;
326+
327+
/** Region of the stack this metric is attached to. */
328+
readonly #stackRegion?: string;
311329

312-
/** Region which this metric comes from. */
313-
public readonly region?: string;
330+
/** Account set directly on the metric, taking precedence over the stack account. */
331+
readonly #accountOverride?: string;
332+
333+
/** Region set directly on the metric, taking precedence over the stack region. */
334+
readonly #regionOverride?: string;
314335

315336
/**
316337
* Warnings attached to this metric.
@@ -352,8 +373,14 @@ export class Metric implements IMetric {
352373
this.label = props.label;
353374
this.color = props.color;
354375
this.unit = props.unit;
355-
this.account = props.account;
356-
this.region = props.region;
376+
this.#accountOverride = props.account;
377+
this.#regionOverride = props.region;
378+
this.#stackAccount = props.stackAccount;
379+
this.#stackRegion = props.stackRegion;
380+
381+
// Make getters enumerable.
382+
makeEnumerable(Metric.prototype, this, 'account');
383+
makeEnumerable(Metric.prototype, this, 'region');
357384
}
358385

359386
/**
@@ -369,8 +396,10 @@ export class Metric implements IMetric {
369396
&& (props.color === undefined || props.color === this.color)
370397
&& (props.statistic === undefined || props.statistic === this.statistic)
371398
&& (props.unit === undefined || props.unit === this.unit)
372-
&& (props.account === undefined || props.account === this.account)
373-
&& (props.region === undefined || props.region === this.region)
399+
&& (props.account === undefined || props.account === this.#accountOverride)
400+
&& (props.region === undefined || props.region === this.#regionOverride)
401+
&& (props.stackAccount === undefined || props.stackAccount === this.#stackAccount)
402+
&& (props.stackRegion === undefined || props.stackRegion === this.#stackRegion)
374403
// For these we're not going to do deep equality, misses some opportunity for optimization
375404
// but that's okay.
376405
&& (props.dimensions === undefined)
@@ -388,8 +417,10 @@ export class Metric implements IMetric {
388417
unit: ifUndefined(props.unit, this.unit),
389418
label: ifUndefined(props.label, this.label),
390419
color: ifUndefined(props.color, this.color),
391-
account: ifUndefined(props.account, this.account),
392-
region: ifUndefined(props.region, this.region),
420+
account: ifUndefined(props.account, this.#accountOverride),
421+
region: ifUndefined(props.region, this.#regionOverride),
422+
stackAccount: ifUndefined(props.stackAccount, this.#stackAccount),
423+
stackRegion: ifUndefined(props.stackRegion, this.#stackRegion),
393424
});
394425
}
395426

@@ -409,11 +440,25 @@ export class Metric implements IMetric {
409440
const stack = cdk.Stack.of(scope);
410441

411442
return this.with({
412-
region: cdk.Token.isUnresolved(stack.region) ? undefined : stack.region,
413-
account: cdk.Token.isUnresolved(stack.account) ? undefined : stack.account,
443+
stackAccount: cdk.Token.isUnresolved(stack.account) ? undefined : stack.account,
444+
stackRegion: cdk.Token.isUnresolved(stack.region) ? undefined : stack.region,
414445
});
415446
}
416447

448+
/**
449+
* Account which this metric comes from.
450+
*/
451+
public get account(): string | undefined {
452+
return this.#accountOverride || this.#stackAccount;
453+
}
454+
455+
/**
456+
* Region which this metric comes from.
457+
*/
458+
public get region(): string | undefined {
459+
return this.#regionOverride || this.#stackRegion;
460+
}
461+
417462
public toMetricConfig(): MetricConfig {
418463
const dims = this.dimensionsAsList();
419464
return {
@@ -426,6 +471,8 @@ export class Metric implements IMetric {
426471
unitFilter: this.unit,
427472
account: this.account,
428473
region: this.region,
474+
accountOverride: this.#accountOverride,
475+
regionOverride: this.#regionOverride,
429476
},
430477
renderingProperties: {
431478
color: this.color,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Make a property from the specified prototype enumerable on the specific instance.
3+
*/
4+
export function makeEnumerable(prototype: object, instance: object, propertyKey: string) {
5+
Object.defineProperty(instance, propertyKey, {
6+
...Object.getOwnPropertyDescriptor(prototype, propertyKey),
7+
enumerable: true,
8+
});
9+
}

packages/aws-cdk-lib/aws-cloudwatch/lib/private/rendering.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,16 @@ function metricGraphJson(metric: IMetric, yAxis?: string, id?: string) {
4747
}
4848

4949
// Metric attributes that are rendered to graph options
50-
if (stat.account) { options.accountId = accountIfDifferentFromStack(stat.account); }
51-
if (stat.region) { options.region = regionIfDifferentFromStack(stat.region); }
50+
if (stat.accountOverride) {
51+
options.accountId = stat.accountOverride;
52+
} else if (stat.account) {
53+
options.accountId = accountIfDifferentFromStack(stat.account);
54+
}
55+
if (stat.regionOverride) {
56+
options.region = stat.regionOverride;
57+
} else if (stat.region) {
58+
options.region = regionIfDifferentFromStack(stat.region);
59+
}
5260
if (stat.period && stat.period.toSeconds() !== 300) { options.period = stat.period.toSeconds(); }
5361
if (stat.statistic && stat.statistic !== 'Average') { options.stat = stat.statistic; }
5462
},

packages/aws-cdk-lib/aws-cloudwatch/test/cross-environment.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,20 @@ describe('cross environment', () => {
5959
graphMetricsAre(new Stack(), graph, [
6060
['Test', 'ACount', { accountId: '1234', region: 'us-north-5' }],
6161
]);
62+
});
6263

64+
test('metric with explicit account and region that match stack will render as-is', () => {
65+
// GIVEN
66+
const graph = new GraphWidget({
67+
left: [
68+
a.with({ account: '1234', region: 'us-north-5' }),
69+
],
70+
});
71+
72+
// THEN
73+
graphMetricsAre(new Stack(undefined, undefined, { env: { region: 'us-north-5', account: '1234' } }), graph, [
74+
['Test', 'ACount', { accountId: '1234', region: 'us-north-5' }],
75+
]);
6376
});
6477

6578
test('metric attached to agnostic stack will not render in agnostic stack', () => {

packages/aws-cdk-lib/aws-cloudwatch/test/metrics.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,35 @@ describe('Metrics', () => {
273273
expect(metric.statistic).toEqual(customStat);
274274
});
275275

276+
test('region and account getters are enumerable', () => {
277+
const metric = new Metric({
278+
namespace: 'Test',
279+
metricName: 'Metric',
280+
period: cdk.Duration.minutes(10),
281+
region: 'test-region',
282+
account: 'test-account',
283+
});
284+
285+
expect(metric.region).toBe('test-region');
286+
expect(metric.account).toBe('test-account');
287+
288+
const metricObject = { ...metric };
289+
expect(metricObject).toEqual(expect.objectContaining({
290+
region: 'test-region',
291+
account: 'test-account',
292+
}));
293+
294+
// Check that private fields are not included.
295+
// @ts-expect-error
296+
expect(metricObject.accountOverride).toBeUndefined();
297+
// @ts-expect-error
298+
expect(metricObject.stackAccount).toBeUndefined();
299+
// @ts-expect-error
300+
expect(metricObject.regionOverride).toBeUndefined();
301+
// @ts-expect-error
302+
expect(metricObject.stackRegion).toBeUndefined();
303+
});
304+
276305
test('statistic is properly parsed', () => {
277306
const checkParsingSingle = (statistic: string, statPrefix: string, statName: string, value: number) => {
278307
const parsed = parseStatistic(statistic);

0 commit comments

Comments
 (0)