Skip to content
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

feat(cloudwatch): throw ValidationErrors instead of untyped Errors #33456

Merged
merged 4 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const enableNoThrowDefaultErrorIn = [
'aws-cloudfront',
'aws-cloudfront-origins',
'aws-cloudtrail',
'aws-cloudwatch',
'aws-cloudwatch-actions',
'aws-elasticloadbalancing',
'aws-elasticloadbalancingv2',
'aws-elasticloadbalancingv2-actions',
Expand Down
14 changes: 7 additions & 7 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { dispatchMetric, metricPeriod } from './private/metric-util';
import { dropUndefined } from './private/object';
import { MetricSet } from './private/rendering';
import { normalizeStatistic, parseStatistic } from './private/statistic';
import { ArnFormat, Lazy, Stack, Token, Annotations } from '../../core';
import { ArnFormat, Lazy, Stack, Token, Annotations, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';

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

if (hasSubmetrics) {
assertSubmetricsCount(expr);
assertSubmetricsCount(self, expr);
}

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

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

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

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

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

Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/composite-alarm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Construct } from 'constructs';
import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base';
import { CfnCompositeAlarm } from './cloudwatch.generated';
import { ArnFormat, Lazy, Names, Stack, Duration } from '../../core';
import { ArnFormat, Lazy, Names, Stack, Duration, ValidationError } from '../../core';
import { addConstructMetadata } from '../../core/lib/metadata-resource';

/**
Expand Down Expand Up @@ -120,14 +120,14 @@ export class CompositeAlarm extends AlarmBase {
addConstructMetadata(this, props);

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

let extensionPeriod = props.actionsSuppressorExtensionPeriod;
let waitPeriod = props.actionsSuppressorWaitPeriod;
if (props.actionsSuppressor === undefined) {
if (extensionPeriod !== undefined || waitPeriod !== undefined) {
throw new Error('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.');
throw new ValidationError('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.', this);
}
} else {
extensionPeriod = extensionPeriod ?? Duration.minutes(1);
Expand Down
10 changes: 5 additions & 5 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CfnDashboard } from './cloudwatch.generated';
import { Column, Row } from './layout';
import { IVariable } from './variable';
import { IWidget } from './widget';
import { Lazy, Resource, Stack, Token, Annotations, Duration } from '../../core';
import { Lazy, Resource, Stack, Token, Annotations, Duration, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';

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

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

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

const dashboard = new CfnDashboard(this, 'Resource', {
Expand Down
12 changes: 6 additions & 6 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export class GaugeWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.metrics);

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

Expand Down Expand Up @@ -440,7 +440,7 @@ export class GraphWidget extends ConcreteWidget {
props.verticalAnnotations?.forEach(annotation => {
const date = annotation.date;
if (!GraphWidget.isIso8601(date)) {
throw new Error(`Given date ${date} is not in ISO 8601 format`);
throw new cdk.UnscopedValidationError(`Given date ${date} is not in ISO 8601 format`);
}
});
this.props = props;
Expand All @@ -449,7 +449,7 @@ export class GraphWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.leftMetrics, ...this.rightMetrics);

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

Expand Down Expand Up @@ -756,7 +756,7 @@ export class TableWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.metrics);

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

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

if (props.setPeriodToTimeRange && props.sparkline) {
throw new Error('You cannot use setPeriodToTimeRange with sparkline');
throw new cdk.UnscopedValidationError('You cannot use setPeriodToTimeRange with sparkline');
}

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

Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/log-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ export class LogQueryWidget extends ConcreteWidget {
this.props = props;

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

if (!!props.queryString === !!props.queryLines) {
throw new Error('Specify exactly one of \'queryString\' and \'queryLines\'');
throw new cdk.UnscopedValidationError('Specify exactly one of \'queryString\' and \'queryLines\'');
}
}

Expand Down
26 changes: 13 additions & 13 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export class Metric implements IMetric {
this.period = props.period || cdk.Duration.minutes(5);
const periodSec = this.period.toSeconds();
if (periodSec !== 1 && periodSec !== 5 && periodSec !== 10 && periodSec !== 30 && periodSec % 60 !== 0) {
throw new Error(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
throw new cdk.UnscopedValidationError(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
}

this.warnings = undefined;
Expand Down Expand Up @@ -485,7 +485,7 @@ export class Metric implements IMetric {
public toAlarmConfig(): MetricAlarmConfig {
const metricConfig = this.toMetricConfig();
if (metricConfig.metricStat === undefined) {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are all unscoped because Metric isn't a Construct. We'd have to send in scope: Construct as a property to all metrics everywhere to get this to be scoped

}

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

return {
Expand Down Expand Up @@ -586,19 +586,19 @@ export class Metric implements IMetric {

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

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

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

Expand All @@ -609,7 +609,7 @@ export class Metric implements IMetric {
function asString(x?: unknown): string | undefined {
if (x === undefined) { return undefined; }
if (typeof x !== 'string') {
throw new Error(`Expected string, got ${x}`);
throw new cdk.UnscopedValidationError(`Expected string, got ${x}`);
}
return x;
}
Expand Down Expand Up @@ -696,7 +696,7 @@ export class MathExpression implements IMetric {

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

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

/**
* @deprecated use toMetricConfig()
*/
public toGraphConfig(): MetricGraphConfig {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
}

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

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Duration } from '../../../core';
import { Duration, UnscopedValidationError } from '../../../core';
import { MathExpression } from '../metric';
import { IMetric, MetricConfig, MetricExpressionConfig, MetricStatConfig } from '../metric-types';

Expand Down Expand Up @@ -119,12 +119,12 @@ export function metricPeriod(metric: IMetric): Duration {
export function dispatchMetric<A, B>(metric: IMetric, fns: { withStat: (x: MetricStatConfig, c: MetricConfig) => A; withExpression: (x: MetricExpressionConfig, c: MetricConfig) => B }): A | B {
const conf = metric.toMetricConfig();
if (conf.metricStat && conf.mathExpression) {
throw new Error('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
throw new UnscopedValidationError('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
} else if (conf.metricStat) {
return fns.withStat(conf.metricStat, conf);
} else if (conf.mathExpression) {
return fns.withExpression(conf.mathExpression, conf);
} else {
throw new Error('Metric object must have either \'metricStat\' or \'mathExpression\'');
throw new UnscopedValidationError('Metric object must have either \'metricStat\' or \'mathExpression\'');
}
}
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cloudwatch/lib/private/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DropEmptyObjectAtTheEndOfAnArray } from './drop-empty-object-at-the-end
import { accountIfDifferentFromStack, regionIfDifferentFromStack } from './env-tokens';
import { dispatchMetric, metricKey } from './metric-util';
import { dropUndefined } from './object';
import { UnscopedValidationError } from '../../../core';
import { IMetric } from '../metric-types';

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

Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cloudwatch/lib/stats.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UnscopedValidationError } from '../../core';

/**
* Factory functions for standard statistics strings
Expand Down Expand Up @@ -197,7 +198,7 @@ export abstract class Stats {

function assertPercentage(x?: number) {
if (x !== undefined && (x < 0 || x > 100)) {
throw new Error(`Expecting a percentage, got: ${x}`);
throw new UnscopedValidationError(`Expecting a percentage, got: ${x}`);
}
}

Expand Down
12 changes: 7 additions & 5 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/variable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnscopedValidationError } from '../../core';

export enum VariableInputType {
/**
* Freeform text input box
Expand Down Expand Up @@ -85,10 +87,10 @@ export abstract class Values {
*/
public static fromSearchComponents(components: SearchComponents): Values {
if (components.dimensions.length === 0) {
throw new Error('Empty dimensions provided. Please specify one dimension at least');
throw new UnscopedValidationError('Empty dimensions provided. Please specify one dimension at least');
}
if (!components.dimensions.includes(components.populateFrom)) {
throw new Error(`populateFrom (${components.populateFrom}) is not present in dimensions`);
throw new UnscopedValidationError(`populateFrom (${components.populateFrom}) is not present in dimensions`);
}
const metricSchema = [components.namespace, ...components.dimensions];
return Values.fromSearch(`{${metricSchema.join(',')}} MetricName=\"${components.metricName}\"`, components.populateFrom);
Expand All @@ -109,7 +111,7 @@ export abstract class Values {
*/
public static fromValues(...values: VariableValue[]): Values {
if (values.length == 0) {
throw new Error('Empty values is not allowed');
throw new UnscopedValidationError('Empty values is not allowed');
}
return new StaticValues(values);
}
Expand Down Expand Up @@ -227,10 +229,10 @@ export interface DashboardVariableOptions {
export class DashboardVariable implements IVariable {
public constructor(private readonly options: DashboardVariableOptions) {
if (options.inputType !== VariableInputType.INPUT && !options.values) {
throw new Error(`Variable with inputType (${options.inputType}) requires values to be set`);
throw new UnscopedValidationError(`Variable with inputType (${options.inputType}) requires values to be set`);
}
if (options.inputType == VariableInputType.INPUT && options.values) {
throw new Error('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
throw new UnscopedValidationError('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
}
}

Expand Down
Loading