Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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 deprecated_apis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ aws-cdk-lib.aws_cloudwatch.IMetric#toAlarmConfig
aws-cdk-lib.aws_cloudwatch.IMetric#toGraphConfig
aws-cdk-lib.aws_cloudwatch.MathExpression#toAlarmConfig
aws-cdk-lib.aws_cloudwatch.MathExpression#toGraphConfig
aws-cdk-lib.aws_cloudwatch.SearchExpression#toAlarmConfig
aws-cdk-lib.aws_cloudwatch.SearchExpression#toGraphConfig
aws-cdk-lib.aws_cloudwatch.Metric#toAlarmConfig
aws-cdk-lib.aws_cloudwatch.Metric#toGraphConfig
aws-cdk-lib.aws_cloudwatch.MetricAlarmConfig
Expand Down
18 changes: 14 additions & 4 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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, ValidationError, AssumptionError } from '../../core';
import { ArnFormat, Lazy, Stack, Token, Annotations, ValidationError, AssumptionError, UnscopedValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
import { propertyInjectable } from '../../core/lib/prop-injectable';

Expand Down Expand Up @@ -475,7 +475,7 @@ export class Alarm extends AlarmBase {
};
},

withExpression() {
withMathExpression() {
// Expand the math expression metric into a set
const mset = new MetricSet<boolean>();
mset.addTopLevel(true, metric);
Expand Down Expand Up @@ -517,7 +517,7 @@ export class Alarm extends AlarmBase {
returnData,
};
},
withExpression(expr, conf) {
withMathExpression(expr, conf) {
const hasSubmetrics = mathExprHasSubmetrics(expr);

if (hasSubmetrics) {
Expand All @@ -534,6 +534,9 @@ export class Alarm extends AlarmBase {
returnData,
};
},
withSearchExpression(_searchExpr, _conf) {
throw new UnscopedValidationError('Search expressions are not supported in CloudWatch Alarms. Use search expressions only in dashboard graphs.');
Copy link
Member

Choose a reason for hiding this comment

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

You can use a scoped Validation Error in this context with the scope of the Alarm.

Suggested change
withSearchExpression(_searchExpr, _conf) {
throw new UnscopedValidationError('Search expressions are not supported in CloudWatch Alarms. Use search expressions only in dashboard graphs.');
withSearchExpression: (_searchExpr, _conf) => { // Use arrow function to not override `this` from the class instance.
throw new ValidationError('Search expressions are not supported in CloudWatch Alarms. Use search expressions only in dashboard graphs.', this);

},
});
}),
} satisfies AlarmMetricFields;
Expand All @@ -544,6 +547,9 @@ export class Alarm extends AlarmBase {

return { props, primaryId };
},
withSearchExpression() {
throw new UnscopedValidationError('Search expressions are not supported in CloudWatch Alarms. Use search expressions only in dashboard graphs.');
Copy link
Member

Choose a reason for hiding this comment

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

Scoped validation error here as well.

},
});
}

Expand Down Expand Up @@ -608,10 +614,14 @@ function isAnomalyDetectionMetric(metric: IMetric): boolean {
// Not an anomaly detection metric
isAnomalyDetection = false;
},
withExpression(expr) {
withMathExpression(expr) {
// Check if the expression is an anomaly detection band
isAnomalyDetection = expr.expression.includes('ANOMALY_DETECTION_BAND');
},
withSearchExpression() {
// Search expressions are not anomaly detection metrics
isAnomalyDetection = false;
},
});

return isAnomalyDetection;
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/metric-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ export interface MetricConfig {
*/
readonly mathExpression?: MetricExpressionConfig;

/**
* In case the metric is a search expression, the details of the search expression
*
* @default - None
*/
readonly searchExpression?: MetricExpressionConfig;

/**
* Additional properties which will be rendered if the metric is used in a dashboard
*
Expand Down
235 changes: 234 additions & 1 deletion packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ export class MathExpression implements IMetric {
withStat() {
// Nothing
},
withExpression(expr) {
withMathExpression(expr) {
for (const [id, subMetric] of Object.entries(expr.usingMetrics)) {
const existing = seen.get(id);
if (existing && metricKey(existing) !== metricKey(subMetric)) {
Expand All @@ -884,6 +884,16 @@ export class MathExpression implements IMetric {
visit(subMetric);
}
},
withSearchExpression(searchExpr) {
for (const [id, subMetric] of Object.entries(searchExpr.usingMetrics)) {
const existing = seen.get(id);
if (existing && metricKey(existing) !== metricKey(subMetric)) {
throw new cdk.UnscopedValidationError(`The ID '${id}' used for two metrics in the search expression: '${subMetric}' and '${existing}'. Rename one.`);
}
seen.set(id, subMetric);
visit(subMetric);
}
},
});
}
}
Expand All @@ -897,6 +907,228 @@ const VARIABLE_PAT = '[a-z][a-zA-Z0-9_]*';
const VALID_VARIABLE = new RegExp(`^${VARIABLE_PAT}$`);
const FIND_VARIABLE = new RegExp(VARIABLE_PAT, 'g');

/**
* Options for SearchExpression
*/
export interface SearchExpressionOptions {
/**
* Label for this search expression when added to a Graph in a Dashboard
*
* You can use [dynamic labels](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/graph-dynamic-labels.html)
* to show summary information about the entire displayed time series
* in the legend. For example, if you use:
*
* ```
* [max: ${MAX}] MySearchExpression
* ```
*
* As the search expression label, the maximum value in the visible range will
* be shown next to the time series name in the graph's legend.
*
* @default - No label
*/
readonly label?: string;

/**
* The hex color code, prefixed with '#' (e.g. '#00ff00'), to use when this search expression is rendered on a graph.
* The `Color` class has a set of standard colors that can be used here.
*
* @default - Automatic color
*/
readonly color?: string;

/**
* The period over which the specified statistic is applied.
*
* @default Duration.minutes(5)
*/
readonly period?: cdk.Duration;

/**
* Account to evaluate search expressions within.
*
* Specifying a searchAccount has no effect to the account used
* for metrics within the expression (passed via usingMetrics).
*
* @default - Deployment account.
*/
readonly searchAccount?: string;

/**
* Region to evaluate search expressions within.
*
* Specifying a searchRegion has no effect to the region used
* for metrics within the expression (passed via usingMetrics).
*
* @default - Deployment region.
*/
readonly searchRegion?: string;
}

/**
* Properties for a SearchExpression
*/
export interface SearchExpressionProps extends SearchExpressionOptions {
/**
* The search expression string.
*
* Search expressions allow you to search for and graph multiple related metrics from a single expression.
* A search expression can return up to 500 time series.
*
* Examples:
* - `SEARCH('{AWS/EC2,InstanceId} CPUUtilization', 'Average', 300)`
* - `SEARCH('{AWS/ApplicationELB,LoadBalancer} RequestCount', 'Sum', 60)`
* - `SEARCH('{MyNamespace,ServiceName} Errors', 'Sum')`
*
* For more information about search expression syntax, see:
* https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/search-expression-syntax.html
*/
readonly expression: string;
}

/**
* A CloudWatch search expression for dynamically finding and graphing multiple related metrics.
*
* Search expressions allow you to search for and graph multiple related metrics from a single expression.
* This is particularly useful when you have dynamic infrastructure where the exact metric names or
* dimensions are not known at deployment time.
*
* Example usage:
* ```ts
* import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
*
* const searchExpression = new cloudwatch.SearchExpression({
* expression: "SEARCH('{AWS/EC2,InstanceId} CPUUtilization', 'Average', 300)",
* label: 'EC2 CPU Utilization',
* period: Duration.minutes(5),
* });
*
* new cloudwatch.GraphWidget({
* title: 'EC2 Metrics',
* left: [searchExpression],
* });
* ```
*
* This class does not represent a resource, so hence is not a construct. Instead,
* SearchExpression is an abstraction that makes it easy to specify search expressions for use in
* graphs and dashboards.
*/
export class SearchExpression implements IMetric {
/**
* The search expression string.
*/
public readonly expression: string;

/**
* Label for this search expression when added to a Graph.
*/
public readonly label?: string;

/**
* The hex color code, prefixed with '#' (e.g. '#00ff00'), to use when this search expression is rendered on a graph.
* The `Color` class has a set of standard colors that can be used here.
*/
public readonly color?: string;

/**
* Aggregation period of this search expression
*/
public readonly period: cdk.Duration;

/**
* Account to evaluate search expressions within.
*/
public readonly searchAccount?: string;

/**
* Region to evaluate search expressions within.
*/
public readonly searchRegion?: string;

/**
* Warnings generated by this search expression
* @deprecated - use warningsV2
*/
public readonly warnings?: string[];

/**
* Warnings generated by this search expression
*/
public readonly warningsV2?: { [id: string]: string };

constructor(props: SearchExpressionProps) {
this.expression = props.expression;
this.label = props.label;
this.color = props.color;
this.period = props.period || cdk.Duration.minutes(5);
this.searchAccount = props.searchAccount;
this.searchRegion = props.searchRegion;

const warnings: { [id: string]: string } = {};

if (Object.keys(warnings).length > 0) {
this.warnings = Array.from(Object.values(warnings));
this.warningsV2 = warnings;
}
}

/**
* Return a copy of SearchExpression with properties changed.
*
* All properties except expression can be changed.
*
* @param props The set of properties to change.
*/
public with(props: SearchExpressionOptions): SearchExpression {
if ((props.label === undefined || props.label === this.label)
&& (props.color === undefined || props.color === this.color)
&& (props.period === undefined || props.period.toSeconds() === this.period.toSeconds())
&& (props.searchAccount === undefined || props.searchAccount === this.searchAccount)
&& (props.searchRegion === undefined || props.searchRegion === this.searchRegion)) {
return this;
}

return new SearchExpression({
expression: this.expression,
label: ifUndefined(props.label, this.label),
color: ifUndefined(props.color, this.color),
period: ifUndefined(props.period, this.period),
searchAccount: ifUndefined(props.searchAccount, this.searchAccount),
searchRegion: ifUndefined(props.searchRegion, this.searchRegion),
});
}

/**
* @deprecated use toMetricConfig()
*/
public toAlarmConfig(): MetricAlarmConfig {
throw new cdk.UnscopedValidationError('Using a search expression is not supported in CloudWatch Alarms. Search expressions can only be used in dashboard graphs.');
}

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

public toMetricConfig(): MetricConfig {
return {
searchExpression: {
period: this.period.toSeconds(),
expression: this.expression,
usingMetrics: {}, // Empty for search expressions as they don't reference other metrics
searchAccount: this.searchAccount,
searchRegion: this.searchRegion,
},
renderingProperties: {
label: this.label,
color: this.color,
},
};
}
}

function validVariableName(x: string) {
return VALID_VARIABLE.test(x);
}
Expand Down Expand Up @@ -1034,3 +1266,4 @@ function matchAll(x: string, re: RegExp): RegExpMatchArray[] {
}
return ret;
}

Loading
Loading