Skip to content

Commit

Permalink
feat(cloudwatch): support for metric math (#5582)
Browse files Browse the repository at this point in the history
Add support for `MathExpression`, which is a new class that can be used
in place of `Metric` objects in graphs and alarms.

Also in this commit, make it very clear what the purpose of the `unit`
property is, and deprecate `toGraphConfig()` and `toAlarmConfig()`.

Fixes #1077, fixes #5449, fixes #5261, fixes #4716.
  • Loading branch information
AhmedSedek authored and rix0rrr committed Jan 3, 2020
1 parent 786f103 commit a7f189e
Show file tree
Hide file tree
Showing 17 changed files with 1,747 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ export interface ScalingInterval {
readonly change: number;
}

function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType {
const statistic = metric.toAlarmConfig().statistic;
function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType | undefined {
const statistic = metric.toMetricConfig().metricStat?.statistic;
if (statistic == null) { return undefined; } // Math expression, don't know aggregation, leave default

switch (statistic) {
case 'Average':
return MetricAggregationType.AVERAGE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {
throw new Error(`Exactly one of 'customMetric' or 'predefinedMetric' must be specified.`);
}

if (props.customMetric && !props.customMetric.toMetricConfig().metricStat) {
throw new Error(`Only direct metrics are supported for Target Tracking. Use Step Scaling or supply a Metric object.`);
}

super(scope, id);

const resource = new CfnScalingPolicy(this, 'Resource', {
Expand All @@ -140,18 +144,18 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {

function renderCustomMetric(metric?: cloudwatch.IMetric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
if (!metric) { return undefined; }
const c = metric.toAlarmConfig();
const c = metric.toMetricConfig().metricStat!;

if (!c.statistic) {
throw new Error('Can only use Average, Minimum, Maximum, SampleCount, Sum statistic for target tracking');
if (c.statistic.startsWith('p')) {
throw new Error(`Cannot use statistic '${c.statistic}' for Target Tracking: only 'Average', 'Minimum', 'Maximum', 'SampleCount', and 'Sum' are supported.`);
}

return {
dimensions: c.dimensions,
metricName: c.metricName,
namespace: c.namespace,
statistic: c.statistic,
unit: c.unit
unit: c.unitFilter,
};
}

Expand Down
6 changes: 4 additions & 2 deletions packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ export class StepScalingPolicy extends cdk.Construct {
}
}

function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType {
const statistic = metric.toAlarmConfig().statistic;
function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType | undefined {
const statistic = metric.toMetricConfig().metricStat?.statistic;
if (statistic === undefined) { return undefined; } // Math expression, don't know aggregation, leave default

switch (statistic) {
case 'Average':
return MetricAggregationType.AVERAGE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {
throw new Error('When tracking the ALBRequestCountPerTarget metric, the ALB identifier must be supplied in resourceLabel');
}

if (props.customMetric && !props.customMetric.toMetricConfig().metricStat) {
throw new Error(`Only direct metrics are supported for Target Tracking. Use Step Scaling or supply a Metric object.`);
}

super(scope, id);

this.resource = new CfnScalingPolicy(this, 'Resource', {
Expand All @@ -141,18 +145,14 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {

function renderCustomMetric(metric?: cloudwatch.IMetric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
if (!metric) { return undefined; }
const c = metric.toAlarmConfig();

if (!c.statistic) {
throw new Error('Can only use Average, Minimum, Maximum, SampleCount, Sum statistic for target tracking');
}
const c = metric.toMetricConfig().metricStat!;

return {
dimensions: c.dimensions,
metricName: c.metricName,
namespace: c.namespace,
statistic: c.statistic,
unit: c.unit
unit: c.unitFilter,
};
}

Expand Down
102 changes: 83 additions & 19 deletions packages/@aws-cdk/aws-cloudwatch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
---
<!--END STABILITY BANNER-->

## Metric objects

Metric objects represent a metric that is emitted by AWS services or your own
application, such as `CPUUsage`, `FailureCount` or `Bandwidth`.

Expand All @@ -24,6 +26,49 @@ represents the amount of errors reported by that Lambda function:
const errors = fn.metricErrors();
```

### Instantiating a new Metric object

If you want to reference a metric that is not yet exposed by an existing construct,
you can instantiate a `Metric` object to represent it. For example:

```ts
const metric = new Metric({
namespace: 'MyNamespace',
metricName: 'MyMetric',
dimensions: {
ProcessingStep: 'Download'
}
});
```

### Metric Math

Math expressions are supported by instantiating the `MathExpression` class.
For example, a math expression that sums two other metrics looks like this:

```ts
const allProblems = new MathExpression({
expression: "errors + faults",
usingMetrics: {
errors: myConstruct.metricErrors(),
faults: myConstruct.metricFaults(),
}
})
```

You can use `MathExpression` objects like any other metric, including using
them in other math expressions:

```ts
const problemPercentage = new MathExpression({
expression: "(problems / invocations) * 100",
usingMetrics: {
problems: allProblems,
invocations: myConstruct.metricInvocations()
}
})
```

### Aggregation

To graph or alarm on metrics you must aggregate them first, using a function
Expand All @@ -40,9 +85,9 @@ to the metric function call:

```ts
const minuteErrorRate = fn.metricErrors({
statistic: 'avg',
period: Duration.minutes(1),
label: 'Lambda failure rate'
statistic: 'avg',
period: Duration.minutes(1),
label: 'Lambda failure rate'
});
```

Expand Down Expand Up @@ -75,18 +120,18 @@ object, passing the `Metric` object to set the alarm on:

```ts
new Alarm(this, 'Alarm', {
metric: fn.metricErrors(),
threshold: 100,
evaluationPeriods: 2,
metric: fn.metricErrors(),
threshold: 100,
evaluationPeriods: 2,
});
```

Alternatively, you can call `metric.createAlarm()`:

```ts
fn.metricErrors().createAlarm(this, 'Alarm', {
threshold: 100,
evaluationPeriods: 2,
threshold: 100,
evaluationPeriods: 2,
});
```

Expand All @@ -97,6 +142,25 @@ The most important properties to set while creating an Alarms are:
- `evaluationPeriods`: how many consecutive periods the metric has to be
breaching the the threshold for the alarm to trigger.

### A note on units

In CloudWatch, Metrics datums are emitted with units, such as `seconds` or
`bytes`. When `Metric` objects are given a `unit` attribute, it will be used to
*filter* the stream of metric datums for datums emitted using the same `unit`
attribute.

In particular, the `unit` field is *not* used to rescale datums or alarm threshold
values (for example, it cannot be used to specify an alarm threshold in
*Megabytes* if the metric stream is being emitted as *bytes*).

You almost certainly don't want to specify the `unit` property when creating
`Metric` objects (which will retrieve all datums regardless of their unit),
unless you have very specific requirements. Note that in any case, CloudWatch
only supports filtering by `unit` for Alarms, not in Dashboard graphs.

Please see the following GitHub issue for a discussion on real unit
calculations in CDK: https://github.com/aws/aws-cdk/issues/5595

## Dashboards

Dashboards are set of Widgets stored server-side which can be accessed quickly
Expand All @@ -119,15 +183,15 @@ A graph widget can display any number of metrics on either the `left` or

```ts
dashboard.addWidgets(new GraphWidget({
title: "Executions vs error rate",
title: "Executions vs error rate",

left: [executionCountMetric],
left: [executionCountMetric],

right: [errorCountMetric.with({
statistic: "average",
label: "Error rate",
color: "00FF00"
})]
right: [errorCountMetric.with({
statistic: "average",
label: "Error rate",
color: "00FF00"
})]
}));
```

Expand All @@ -137,8 +201,8 @@ An alarm widget shows the graph and the alarm line of a single alarm:

```ts
dashboard.addWidgets(new AlarmWidget({
title: "Errors",
alarm: errorAlarm,
title: "Errors",
alarm: errorAlarm,
}));
```

Expand All @@ -149,7 +213,7 @@ to a graph of the value over time):

```ts
dashboard.addWidgets(new SingleValueWidget({
metrics: [visitorCount, purchaseCount],
metrics: [visitorCount, purchaseCount],
}));
```

Expand All @@ -160,7 +224,7 @@ to your dashboard:

```ts
dashboard.addWidgets(new TextWidget({
markdown: '# Key Performance Indicators'
markdown: '# Key Performance Indicators'
}));
```

Expand Down
78 changes: 62 additions & 16 deletions packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CfnAlarm } from './cloudwatch.generated';
import { HorizontalAnnotation } from './graph';
import { CreateAlarmOptions } from './metric';
import { IMetric } from './metric-types';
import { dispatchMetric, dropUndefined, metricPeriod, MetricSet } from './metric-util';
import { parseStatistic } from './util.statistic';

export interface IAlarm extends IResource {
Expand Down Expand Up @@ -121,8 +122,6 @@ export class Alarm extends Resource implements IAlarm {

const comparisonOperator = props.comparisonOperator || ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD;

const config = props.metric.toAlarmConfig();

const alarm = new CfnAlarm(this, 'Resource', {
// Meta
alarmDescription: props.alarmDescription,
Expand All @@ -143,8 +142,8 @@ export class Alarm extends Resource implements IAlarm {
okActions: Lazy.listValue({ produce: () => this.okActionArns }),

// Metric
...dropUndef(config),
...dropUndef({
...renderAlarmMetric(props.metric),
...dropUndefined({
// Alarm overrides
period: props.period && props.period.toSeconds(),
statistic: renderIfSimpleStatistic(props.statistic),
Expand All @@ -163,7 +162,7 @@ export class Alarm extends Resource implements IAlarm {
this.metric = props.metric;
this.annotation = {
// tslint:disable-next-line:max-line-length
label: `${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * config.period)}`,
label: `${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * metricPeriod(props.metric).toSeconds())}`,
value: props.threshold,
};
}
Expand Down Expand Up @@ -228,6 +227,63 @@ export class Alarm extends Resource implements IAlarm {
}
}

function renderAlarmMetric(metric: IMetric) {
return dispatchMetric(metric, {
withStat(st) {
return dropUndefined({
dimensions: st.dimensions,
namespace: st.namespace,
metricName: st.metricName,
period: st.period?.toSeconds(),
statistic: renderIfSimpleStatistic(st.statistic),
extendedStatistic: renderIfExtendedStatistic(st.statistic),
unit: st.unitFilter,
});
},

withExpression() {
// Expand the math expression metric into a set
const mset = new MetricSet<boolean>();
mset.addTopLevel(true, metric);

let eid = 0;
function uniqueMetricId() {
return `expr_${++eid}`;
}

return {
metrics: mset.entries.map(entry => dispatchMetric(entry.metric, {
withStat(stat, conf) {
return {
metricStat: {
metric: {
metricName: stat.metricName,
namespace: stat.namespace,
dimensions: stat.dimensions,
},
period: stat.period.toSeconds(),
stat: stat.statistic,
unit: stat.unitFilter,
},
id: entry.id ?? uniqueMetricId(),
label: conf.renderingProperties?.label,
returnData: entry.tag ? undefined : false, // Tag stores "primary" attribute, default is "true"
};
},
withExpression(expr, conf) {
return {
expression: expr.expression,
id: entry.id ?? uniqueMetricId(),
label: conf.renderingProperties?.label,
returnData: entry.tag ? undefined : false, // Tag stores "primary" attribute, default is "true"
};
},
}) as CfnAlarm.MetricDataQueryProperty)
};
}
});
}

/**
* Return a human readable string for this period
*
Expand All @@ -240,16 +296,6 @@ function describePeriod(seconds: number) {
return seconds + ' seconds';
}

function dropUndef<T extends object>(x: T): T {
const ret: any = {};
for (const [key, value] of Object.entries(x)) {
if (value !== undefined) {
ret[key] = value;
}
}
return ret;
}

function renderIfSimpleStatistic(statistic?: string): string | undefined {
if (statistic === undefined) { return undefined; }

Expand All @@ -270,4 +316,4 @@ function renderIfExtendedStatistic(statistic?: string): string | undefined {
return statistic.toLowerCase();
}
return undefined;
}
}
Loading

0 comments on commit a7f189e

Please sign in to comment.