Skip to content

Commit 6098816

Browse files
authored
feat(cloudwatch): throw ValidationErrors instead of untyped Errors (#33456)
### Issue Relates to #32569 ### Description of changes `ValidationErrors` everywhere ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is a refactor of existing code. ### 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 50b9b6f commit 6098816

File tree

12 files changed

+54
-47
lines changed

12 files changed

+54
-47
lines changed

packages/aws-cdk-lib/.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const enableNoThrowDefaultErrorIn = [
3838
'aws-cloudfront',
3939
'aws-cloudfront-origins',
4040
'aws-cloudtrail',
41+
'aws-cloudwatch',
42+
'aws-cloudwatch-actions',
4143
'aws-elasticloadbalancing',
4244
'aws-elasticloadbalancingv2',
4345
'aws-elasticloadbalancingv2-actions',

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { dispatchMetric, metricPeriod } from './private/metric-util';
99
import { dropUndefined } from './private/object';
1010
import { MetricSet } from './private/rendering';
1111
import { normalizeStatistic, parseStatistic } from './private/statistic';
12-
import { ArnFormat, Lazy, Stack, Token, Annotations } from '../../core';
12+
import { ArnFormat, Lazy, Stack, Token, Annotations, ValidationError } from '../../core';
1313
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
1414

1515
/**
@@ -272,7 +272,7 @@ export class Alarm extends AlarmBase {
272272
// Check per-instance metric
273273
const metricConfig = this.metric.toMetricConfig();
274274
if (metricConfig.metricStat?.dimensions?.length != 1 || !metricConfig.metricStat?.dimensions?.some(dimension => dimension.name === 'InstanceId')) {
275-
throw new Error(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`);
275+
throw new ValidationError(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`, this);
276276
}
277277
}
278278
return actionArn;
@@ -355,7 +355,7 @@ export class Alarm extends AlarmBase {
355355
const hasSubmetrics = mathExprHasSubmetrics(expr);
356356

357357
if (hasSubmetrics) {
358-
assertSubmetricsCount(expr);
358+
assertSubmetricsCount(self, expr);
359359
}
360360

361361
self.validateMetricExpression(expr);
@@ -381,7 +381,7 @@ export class Alarm extends AlarmBase {
381381
const stack = Stack.of(this);
382382

383383
if (definitelyDifferent(stat.region, stack.region)) {
384-
throw new Error(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`);
384+
throw new ValidationError(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`, this);
385385
}
386386
}
387387

@@ -391,7 +391,7 @@ export class Alarm extends AlarmBase {
391391
*/
392392
private validateMetricExpression(expr: MetricExpressionConfig) {
393393
if (expr.searchAccount !== undefined || expr.searchRegion !== undefined) {
394-
throw new Error('Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion');
394+
throw new ValidationError('Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion', this);
395395
}
396396
}
397397

@@ -462,10 +462,10 @@ function mathExprHasSubmetrics(expr: MetricExpressionConfig) {
462462
return Object.keys(expr.usingMetrics).length > 0;
463463
}
464464

465-
function assertSubmetricsCount(expr: MetricExpressionConfig) {
465+
function assertSubmetricsCount(scope: Construct, expr: MetricExpressionConfig) {
466466
if (Object.keys(expr.usingMetrics).length > 10) {
467467
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-on-metric-math-expressions
468-
throw new Error('Alarms on math expressions cannot contain more than 10 individual metrics');
468+
throw new ValidationError('Alarms on math expressions cannot contain more than 10 individual metrics', scope);
469469
}
470470
}
471471

packages/aws-cdk-lib/aws-cloudwatch/lib/composite-alarm.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Construct } from 'constructs';
22
import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base';
33
import { CfnCompositeAlarm } from './cloudwatch.generated';
4-
import { ArnFormat, Lazy, Names, Stack, Duration } from '../../core';
4+
import { ArnFormat, Lazy, Names, Stack, Duration, ValidationError } from '../../core';
55
import { addConstructMetadata } from '../../core/lib/metadata-resource';
66

77
/**
@@ -120,14 +120,14 @@ export class CompositeAlarm extends AlarmBase {
120120
addConstructMetadata(this, props);
121121

122122
if (props.alarmRule.renderAlarmRule().length > 10240) {
123-
throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule');
123+
throw new ValidationError('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule', this);
124124
}
125125

126126
let extensionPeriod = props.actionsSuppressorExtensionPeriod;
127127
let waitPeriod = props.actionsSuppressorWaitPeriod;
128128
if (props.actionsSuppressor === undefined) {
129129
if (extensionPeriod !== undefined || waitPeriod !== undefined) {
130-
throw new Error('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.');
130+
throw new ValidationError('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.', this);
131131
}
132132
} else {
133133
extensionPeriod = extensionPeriod ?? Duration.minutes(1);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CfnDashboard } from './cloudwatch.generated';
33
import { Column, Row } from './layout';
44
import { IVariable } from './variable';
55
import { IWidget } from './widget';
6-
import { Lazy, Resource, Stack, Token, Annotations, Duration } from '../../core';
6+
import { Lazy, Resource, Stack, Token, Annotations, Duration, ValidationError } from '../../core';
77
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
88

99
/**
@@ -127,19 +127,19 @@ export class Dashboard extends Resource {
127127
{
128128
const { dashboardName } = props;
129129
if (dashboardName && !Token.isUnresolved(dashboardName) && !dashboardName.match(/^[\w-]+$/)) {
130-
throw new Error([
130+
throw new ValidationError([
131131
`The value ${dashboardName} for field dashboardName contains invalid characters.`,
132132
'It can only contain alphanumerics, dash (-) and underscore (_).',
133-
].join(' '));
133+
].join(' '), this);
134134
}
135135
}
136136

137137
if (props.start !== undefined && props.defaultInterval !== undefined) {
138-
throw new Error('both properties defaultInterval and start cannot be set at once');
138+
throw new ValidationError('both properties defaultInterval and start cannot be set at once', this);
139139
}
140140

141141
if (props.end !== undefined && props.start === undefined) {
142-
throw new Error('If you specify a value for end, you must also specify a value for start.');
142+
throw new ValidationError('If you specify a value for end, you must also specify a value for start.', this);
143143
}
144144

145145
const dashboard = new CfnDashboard(this, 'Resource', {

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class GaugeWidget extends ConcreteWidget {
242242
this.copyMetricWarnings(...this.metrics);
243243

244244
if (props.end !== undefined && props.start === undefined) {
245-
throw new Error('If you specify a value for end, you must also specify a value for start.');
245+
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
246246
}
247247
}
248248

@@ -440,7 +440,7 @@ export class GraphWidget extends ConcreteWidget {
440440
props.verticalAnnotations?.forEach(annotation => {
441441
const date = annotation.date;
442442
if (!GraphWidget.isIso8601(date)) {
443-
throw new Error(`Given date ${date} is not in ISO 8601 format`);
443+
throw new cdk.UnscopedValidationError(`Given date ${date} is not in ISO 8601 format`);
444444
}
445445
});
446446
this.props = props;
@@ -449,7 +449,7 @@ export class GraphWidget extends ConcreteWidget {
449449
this.copyMetricWarnings(...this.leftMetrics, ...this.rightMetrics);
450450

451451
if (props.end !== undefined && props.start === undefined) {
452-
throw new Error('If you specify a value for end, you must also specify a value for start.');
452+
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
453453
}
454454
}
455455

@@ -756,7 +756,7 @@ export class TableWidget extends ConcreteWidget {
756756
this.copyMetricWarnings(...this.metrics);
757757

758758
if (props.end !== undefined && props.start === undefined) {
759-
throw new Error('If you specify a value for end, you must also specify a value for start.');
759+
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
760760
}
761761
}
762762

@@ -885,11 +885,11 @@ export class SingleValueWidget extends ConcreteWidget {
885885
this.copyMetricWarnings(...props.metrics);
886886

887887
if (props.setPeriodToTimeRange && props.sparkline) {
888-
throw new Error('You cannot use setPeriodToTimeRange with sparkline');
888+
throw new cdk.UnscopedValidationError('You cannot use setPeriodToTimeRange with sparkline');
889889
}
890890

891891
if (props.end !== undefined && props.start === undefined) {
892-
throw new Error('If you specify a value for end, you must also specify a value for start.');
892+
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
893893
}
894894
}
895895

packages/aws-cdk-lib/aws-cloudwatch/lib/log-query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export class LogQueryWidget extends ConcreteWidget {
102102
this.props = props;
103103

104104
if (props.logGroupNames.length === 0) {
105-
throw new Error('Specify at least one log group name.');
105+
throw new cdk.UnscopedValidationError('Specify at least one log group name.');
106106
}
107107

108108
if (!!props.queryString === !!props.queryLines) {
109-
throw new Error('Specify exactly one of \'queryString\' and \'queryLines\'');
109+
throw new cdk.UnscopedValidationError('Specify exactly one of \'queryString\' and \'queryLines\'');
110110
}
111111
}
112112

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ export class Metric implements IMetric {
346346
this.period = props.period || cdk.Duration.minutes(5);
347347
const periodSec = this.period.toSeconds();
348348
if (periodSec !== 1 && periodSec !== 5 && periodSec !== 10 && periodSec !== 30 && periodSec % 60 !== 0) {
349-
throw new Error(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
349+
throw new cdk.UnscopedValidationError(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
350350
}
351351

352352
this.warnings = undefined;
@@ -485,7 +485,7 @@ export class Metric implements IMetric {
485485
public toAlarmConfig(): MetricAlarmConfig {
486486
const metricConfig = this.toMetricConfig();
487487
if (metricConfig.metricStat === undefined) {
488-
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
488+
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
489489
}
490490

491491
const parsed = parseStatistic(metricConfig.metricStat.statistic);
@@ -514,7 +514,7 @@ export class Metric implements IMetric {
514514
public toGraphConfig(): MetricGraphConfig {
515515
const metricConfig = this.toMetricConfig();
516516
if (metricConfig.metricStat === undefined) {
517-
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
517+
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
518518
}
519519

520520
return {
@@ -586,19 +586,19 @@ export class Metric implements IMetric {
586586

587587
var dimsArray = Object.keys(dims);
588588
if (dimsArray?.length > 30) {
589-
throw new Error(`The maximum number of dimensions is 30, received ${dimsArray.length}`);
589+
throw new cdk.UnscopedValidationError(`The maximum number of dimensions is 30, received ${dimsArray.length}`);
590590
}
591591

592592
dimsArray.map(key => {
593593
if (dims[key] === undefined || dims[key] === null) {
594-
throw new Error(`Dimension value of '${dims[key]}' is invalid`);
594+
throw new cdk.UnscopedValidationError(`Dimension value of '${dims[key]}' is invalid`);
595595
}
596596
if (key.length < 1 || key.length > 255) {
597-
throw new Error(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`);
597+
throw new cdk.UnscopedValidationError(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`);
598598
}
599599

600600
if (dims[key].length < 1 || dims[key].length > 255) {
601-
throw new Error(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`);
601+
throw new cdk.UnscopedValidationError(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`);
602602
}
603603
});
604604

@@ -609,7 +609,7 @@ export class Metric implements IMetric {
609609
function asString(x?: unknown): string | undefined {
610610
if (x === undefined) { return undefined; }
611611
if (typeof x !== 'string') {
612-
throw new Error(`Expected string, got ${x}`);
612+
throw new cdk.UnscopedValidationError(`Expected string, got ${x}`);
613613
}
614614
return x;
615615
}
@@ -696,7 +696,7 @@ export class MathExpression implements IMetric {
696696

697697
const invalidVariableNames = Object.keys(this.usingMetrics).filter(x => !validVariableName(x));
698698
if (invalidVariableNames.length > 0) {
699-
throw new Error(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`);
699+
throw new cdk.UnscopedValidationError(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`);
700700
}
701701

702702
this.validateNoIdConflicts();
@@ -756,14 +756,14 @@ export class MathExpression implements IMetric {
756756
* @deprecated use toMetricConfig()
757757
*/
758758
public toAlarmConfig(): MetricAlarmConfig {
759-
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
759+
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
760760
}
761761

762762
/**
763763
* @deprecated use toMetricConfig()
764764
*/
765765
public toGraphConfig(): MetricGraphConfig {
766-
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
766+
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
767767
}
768768

769769
public toMetricConfig(): MetricConfig {
@@ -822,7 +822,7 @@ export class MathExpression implements IMetric {
822822
for (const [id, subMetric] of Object.entries(expr.usingMetrics)) {
823823
const existing = seen.get(id);
824824
if (existing && metricKey(existing) !== metricKey(subMetric)) {
825-
throw new Error(`The ID '${id}' used for two metrics in the expression: '${subMetric}' and '${existing}'. Rename one.`);
825+
throw new cdk.UnscopedValidationError(`The ID '${id}' used for two metrics in the expression: '${subMetric}' and '${existing}'. Rename one.`);
826826
}
827827
seen.set(id, subMetric);
828828
visit(subMetric);
@@ -990,7 +990,7 @@ function changePeriod(metric: IMetric, period: cdk.Duration): { metric: IMetric;
990990
return { metric: metric.with({ period }), overridden };
991991
}
992992

993-
throw new Error(`Metric object should also implement 'with': ${metric}`);
993+
throw new cdk.UnscopedValidationError(`Metric object should also implement 'with': ${metric}`);
994994
}
995995

996996
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Duration } from '../../../core';
1+
import { Duration, UnscopedValidationError } from '../../../core';
22
import { MathExpression } from '../metric';
33
import { IMetric, MetricConfig, MetricExpressionConfig, MetricStatConfig } from '../metric-types';
44

@@ -119,12 +119,12 @@ export function metricPeriod(metric: IMetric): Duration {
119119
export function dispatchMetric<A, B>(metric: IMetric, fns: { withStat: (x: MetricStatConfig, c: MetricConfig) => A; withExpression: (x: MetricExpressionConfig, c: MetricConfig) => B }): A | B {
120120
const conf = metric.toMetricConfig();
121121
if (conf.metricStat && conf.mathExpression) {
122-
throw new Error('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
122+
throw new UnscopedValidationError('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
123123
} else if (conf.metricStat) {
124124
return fns.withStat(conf.metricStat, conf);
125125
} else if (conf.mathExpression) {
126126
return fns.withExpression(conf.mathExpression, conf);
127127
} else {
128-
throw new Error('Metric object must have either \'metricStat\' or \'mathExpression\'');
128+
throw new UnscopedValidationError('Metric object must have either \'metricStat\' or \'mathExpression\'');
129129
}
130130
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DropEmptyObjectAtTheEndOfAnArray } from './drop-empty-object-at-the-end
22
import { accountIfDifferentFromStack, regionIfDifferentFromStack } from './env-tokens';
33
import { dispatchMetric, metricKey } from './metric-util';
44
import { dropUndefined } from './object';
5+
import { UnscopedValidationError } from '../../../core';
56
import { IMetric } from '../metric-types';
67

78
/**
@@ -159,7 +160,7 @@ export class MetricSet<A> {
159160
if (id) {
160161
existingEntry = this.metricById.get(id);
161162
if (existingEntry && metricKey(existingEntry.metric) !== key) {
162-
throw new Error(`Cannot have two different metrics share the same id ('${id}') in one Alarm or Graph. Rename one of them.`);
163+
throw new UnscopedValidationError(`Cannot have two different metrics share the same id ('${id}') in one Alarm or Graph. Rename one of them.`);
163164
}
164165
}
165166

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { UnscopedValidationError } from '../../core';
12

23
/**
34
* Factory functions for standard statistics strings
@@ -197,7 +198,7 @@ export abstract class Stats {
197198

198199
function assertPercentage(x?: number) {
199200
if (x !== undefined && (x < 0 || x > 100)) {
200-
throw new Error(`Expecting a percentage, got: ${x}`);
201+
throw new UnscopedValidationError(`Expecting a percentage, got: ${x}`);
201202
}
202203
}
203204

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { UnscopedValidationError } from '../../core';
2+
13
export enum VariableInputType {
24
/**
35
* Freeform text input box
@@ -85,10 +87,10 @@ export abstract class Values {
8587
*/
8688
public static fromSearchComponents(components: SearchComponents): Values {
8789
if (components.dimensions.length === 0) {
88-
throw new Error('Empty dimensions provided. Please specify one dimension at least');
90+
throw new UnscopedValidationError('Empty dimensions provided. Please specify one dimension at least');
8991
}
9092
if (!components.dimensions.includes(components.populateFrom)) {
91-
throw new Error(`populateFrom (${components.populateFrom}) is not present in dimensions`);
93+
throw new UnscopedValidationError(`populateFrom (${components.populateFrom}) is not present in dimensions`);
9294
}
9395
const metricSchema = [components.namespace, ...components.dimensions];
9496
return Values.fromSearch(`{${metricSchema.join(',')}} MetricName=\"${components.metricName}\"`, components.populateFrom);
@@ -109,7 +111,7 @@ export abstract class Values {
109111
*/
110112
public static fromValues(...values: VariableValue[]): Values {
111113
if (values.length == 0) {
112-
throw new Error('Empty values is not allowed');
114+
throw new UnscopedValidationError('Empty values is not allowed');
113115
}
114116
return new StaticValues(values);
115117
}
@@ -227,10 +229,10 @@ export interface DashboardVariableOptions {
227229
export class DashboardVariable implements IVariable {
228230
public constructor(private readonly options: DashboardVariableOptions) {
229231
if (options.inputType !== VariableInputType.INPUT && !options.values) {
230-
throw new Error(`Variable with inputType (${options.inputType}) requires values to be set`);
232+
throw new UnscopedValidationError(`Variable with inputType (${options.inputType}) requires values to be set`);
231233
}
232234
if (options.inputType == VariableInputType.INPUT && options.values) {
233-
throw new Error('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
235+
throw new UnscopedValidationError('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
234236
}
235237
}
236238

0 commit comments

Comments
 (0)