Skip to content

Commit

Permalink
refactor(cloudwatch): cleanup of CloudWatch Metrics API (#2839)
Browse files Browse the repository at this point in the history
Generalize `Metric` to `IMetric`, so that we can use it to add metric
math later.

BREAKING CHANGES:

* **cloudwatch** rename `metric.newAlarm` to `metric.createAlarm`,
  the options object has been commensurately renamed.
* **cloudwatch** rename `dashboard.add()` to `dashboard.addWidgets()`.
  • Loading branch information
rix0rrr authored Jun 18, 2019
1 parent 1e23921 commit 23a143e
Show file tree
Hide file tree
Showing 19 changed files with 407 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface BasicStepScalingPolicyProps {
/**
* Metric to scale on.
*/
readonly metric: cloudwatch.Metric;
readonly metric: cloudwatch.IMetric;

/**
* The intervals for scaling.
Expand Down Expand Up @@ -101,8 +101,8 @@ export class StepScalingPolicy extends cdk.Construct {
}

this.lowerAlarm = new cloudwatch.Alarm(this, 'LowerAlarm', {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
metric: props.metric,
periodSec: 60, // Recommended by AutoScaling
alarmDescription: 'Lower threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.LessThanOrEqualToThreshold,
evaluationPeriods: 1,
Expand Down Expand Up @@ -131,8 +131,8 @@ export class StepScalingPolicy extends cdk.Construct {
}

this.upperAlarm = new cloudwatch.Alarm(this, 'UpperAlarm', {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
metric: props.metric,
periodSec: 60, // Recommended by AutoScaling
alarmDescription: 'Upper threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.GreaterThanOrEqualToThreshold,
evaluationPeriods: 1,
Expand Down Expand Up @@ -180,16 +180,17 @@ export interface ScalingInterval {
readonly change: number;
}

function aggregationTypeFromMetric(metric: cloudwatch.Metric): MetricAggregationType {
switch (metric.statistic) {
function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType {
const statistic = metric.toAlarmConfig().statistic;
switch (statistic) {
case 'Average':
return MetricAggregationType.Average;
case 'Minimum':
return MetricAggregationType.Minimum;
case 'Maximum':
return MetricAggregationType.Maximum;
default:
throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${metric.statistic}`);
throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${statistic}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface BasicTargetTrackingScalingPolicyProps extends BaseTargetTrackin
*
* @default - No custom metric.
*/
readonly customMetric?: cloudwatch.Metric;
readonly customMetric?: cloudwatch.IMetric;
}

/**
Expand Down Expand Up @@ -145,14 +145,20 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {
}
}

function renderCustomMetric(metric?: cloudwatch.Metric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
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');
}

return {
dimensions: metric.dimensionsAsList(),
metricName: metric.metricName,
namespace: metric.namespace,
statistic: metric.statistic,
unit: metric.unit
dimensions: c.dimensions,
metricName: c.metricName,
namespace: c.namespace,
statistic: c.statistic,
unit: c.unit
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export = {
PolicyType: "TargetTrackingScaling",
TargetTrackingScalingPolicyConfiguration: {
CustomizedMetricSpecification: {
Dimensions: [],
MetricName: "Metric",
Namespace: "Test",
Statistic: "Average"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ export interface MetricTargetTrackingProps extends BaseTargetTrackingProps {
* target value, your ASG should scale out, and if it's lower it should
* scale in.
*/
readonly metric: cloudwatch.Metric;
readonly metric: cloudwatch.IMetric;

/**
* Value to keep the metric around
Expand Down
16 changes: 9 additions & 7 deletions packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface BasicStepScalingPolicyProps {
/**
* Metric to scale on.
*/
readonly metric: cloudwatch.Metric;
readonly metric: cloudwatch.IMetric;

/**
* The intervals for scaling.
Expand Down Expand Up @@ -102,8 +102,8 @@ export class StepScalingPolicy extends cdk.Construct {
}

this.lowerAlarm = new cloudwatch.Alarm(this, 'LowerAlarm', {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
metric: props.metric,
periodSec: 60, // Recommended by AutoScaling
alarmDescription: 'Lower threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.LessThanOrEqualToThreshold,
evaluationPeriods: 1,
Expand Down Expand Up @@ -133,7 +133,8 @@ export class StepScalingPolicy extends cdk.Construct {

this.upperAlarm = new cloudwatch.Alarm(this, 'UpperAlarm', {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
metric: props.metric,
periodSec: 60, // Recommended by AutoScaling
alarmDescription: 'Upper threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.GreaterThanOrEqualToThreshold,
evaluationPeriods: 1,
Expand All @@ -144,16 +145,17 @@ export class StepScalingPolicy extends cdk.Construct {
}
}

function aggregationTypeFromMetric(metric: cloudwatch.Metric): MetricAggregationType {
switch (metric.statistic) {
function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType {
const statistic = metric.toAlarmConfig().statistic;
switch (statistic) {
case 'Average':
return MetricAggregationType.Average;
case 'Minimum':
return MetricAggregationType.Minimum;
case 'Maximum':
return MetricAggregationType.Maximum;
default:
throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${metric.statistic}`);
throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${statistic}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface BasicTargetTrackingScalingPolicyProps extends BaseTargetTrackin
*
* @default - No custom metric.
*/
readonly customMetric?: cloudwatch.Metric;
readonly customMetric?: cloudwatch.IMetric;

/**
* The resource label associated with the predefined metric
Expand Down Expand Up @@ -147,14 +147,20 @@ export class TargetTrackingScalingPolicy extends cdk.Construct {
}
}

function renderCustomMetric(metric?: cloudwatch.Metric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
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');
}

return {
dimensions: metric.dimensionsAsList(),
metricName: metric.metricName,
namespace: metric.namespace,
statistic: metric.statistic,
unit: metric.unit
dimensions: c.dimensions,
metricName: c.metricName,
namespace: c.namespace,
statistic: c.statistic,
unit: c.unit
};
}

Expand Down
89 changes: 24 additions & 65 deletions packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/cdk';
import { IAlarmAction } from './alarm-action';
import { CfnAlarm } from './cloudwatch.generated';
import { HorizontalAnnotation } from './graph';
import { Dimension, Metric, MetricAlarmProps, Statistic, Unit } from './metric';
import { parseStatistic } from './util.statistic';
import { CreateAlarmOptions } from './metric';
import { IMetric } from './metric-types';
import { normalizeStatistic } from './util.statistic';

export interface IAlarm extends IResource {
/**
Expand All @@ -20,14 +21,14 @@ export interface IAlarm extends IResource {
/**
* Properties for Alarms
*/
export interface AlarmProps extends MetricAlarmProps {
export interface AlarmProps extends CreateAlarmOptions {
/**
* The metric to add the alarm on
*
* Metric objects can be obtained from most resources, or you can construct
* custom Metric objects by instantiating one.
*/
readonly metric: Metric;
readonly metric: IMetric;
}

/**
Expand Down Expand Up @@ -102,7 +103,7 @@ export class Alarm extends Resource implements IAlarm {
/**
* The metric object this alarm was based on
*/
public readonly metric: Metric;
public readonly metric: IMetric;

private alarmActionArns?: string[];
private insufficientDataActionArns?: string[];
Expand All @@ -118,6 +119,8 @@ export class Alarm extends Resource implements IAlarm {

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

const config = props.metric.toAlarmConfig();

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

// Metric
...metricJson(props.metric)
...dropUndef(config),
...dropUndef({
// Alarm overrides
period: props.periodSec,
statistic: props.statistic && normalizeStatistic(props.statistic),
})
});

this.alarmArn = alarm.attrArn;
this.alarmName = alarm.refAsString;
this.metric = props.metric;
this.annotation = {
// tslint:disable-next-line:max-line-length
label: `${this.metric.label || this.metric.metricName} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * props.metric.periodSec)}`,
label: `${this.metric} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * config.period)}`,
value: props.threshold,
};
}
Expand Down Expand Up @@ -223,61 +231,12 @@ function describePeriod(seconds: number) {
return seconds + ' seconds';
}

/**
* Return the JSON structure which represents the given metric in an alarm.
*/
function metricJson(metric: Metric): AlarmMetricJson {
const stat = parseStatistic(metric.statistic);

const dims = metric.dimensionsAsList();

return {
dimensions: dims.length > 0 ? dims : undefined,
namespace: metric.namespace,
metricName: metric.metricName,
period: metric.periodSec,
statistic: stat.type === 'simple' ? stat.statistic : undefined,
extendedStatistic: stat.type === 'percentile' ? 'p' + stat.percentile : undefined,
unit: metric.unit
};
}

/**
* Properties used to construct the Metric identifying part of an Alarm
*/
export interface AlarmMetricJson {
/**
* The dimensions to apply to the alarm
*/
readonly dimensions?: Dimension[];

/**
* Namespace of the metric
*/
readonly namespace: string;

/**
* Name of the metric
*/
readonly metricName: string;

/**
* How many seconds to aggregate over
*/
readonly period: number;

/**
* Simple aggregation function to use
*/
readonly statistic?: Statistic;

/**
* Percentile aggregation function to use
*/
readonly extendedStatistic?: string;

/**
* The unit of the alarm
*/
readonly unit?: Unit;
}
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;
}
25 changes: 19 additions & 6 deletions packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ export interface DashboardProps {
* @default Auto
*/
readonly periodOverride?: PeriodOverride;

/**
* Initial set of widgets on the dashboard
*
* One array represents a row of widgets.
*
* @default - No widgets
*/
readonly widgets?: IWidget[][]
}

/**
Expand All @@ -53,22 +62,26 @@ export interface DashboardProps {
export class Dashboard extends Resource {
private readonly rows: IWidget[] = [];

constructor(scope: Construct, id: string, props?: DashboardProps) {
constructor(scope: Construct, id: string, props: DashboardProps = {}) {
super(scope, id);

new CfnDashboard(this, 'Resource', {
dashboardName: (props && props.dashboardName) || undefined,
dashboardName: props.dashboardName,
dashboardBody: Lazy.stringValue({ produce: () => {
const column = new Column(...this.rows);
column.position(0, 0);
return Stack.of(this).toJsonString({
start: props ? props.start : undefined,
end: props ? props.end : undefined,
periodOverride: props ? props.periodOverride : undefined,
start: props.start,
end: props.end,
periodOverride: props.periodOverride,
widgets: column.toJson(),
});
}})
});

(props.widgets || []).forEach(row => {
this.addWidgets(...row);
});
}

/**
Expand All @@ -80,7 +93,7 @@ export class Dashboard extends Resource {
* Multiple widgets added in the same call to add() will be laid out next
* to each other.
*/
public add(...widgets: IWidget[]) {
public addWidgets(...widgets: IWidget[]) {
if (widgets.length === 0) {
return;
}
Expand Down
Loading

0 comments on commit 23a143e

Please sign in to comment.