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

@aws-cdk/cloudwatch: bugfixes, small changes changes and workaround #194

Merged
merged 2 commits into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 1 addition & 1 deletion packages/@aws-cdk/assert/lib/assertions/have-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StackInspector } from "../inspector";
* Properties can be:
*
* - An object, in which case its properties will be compared to those of the actual resource found
* - A callablage, in which case it will be treated as a predicate that is applied to the Properties of the found resources.
* - A callable, in which case it will be treated as a predicate that is applied to the Properties of the found resources.
*/
export function haveResource(resourceType: string, properties?: any): Assertion<StackInspector> {
return new HaveResourceAssertion(resourceType, properties);
Expand Down
14 changes: 7 additions & 7 deletions packages/@aws-cdk/cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class Alarm extends Construct {
okActions: new Token(() => this.okActions),

// Metric
...props.metric.toAlarmJson()
...props.metric.alarmInfo()
});

this.alarmArn = alarm.alarmArn;
Expand All @@ -175,38 +175,38 @@ export class Alarm extends Construct {
*
* Typically the ARN of an SNS topic or ARN of an AutoScaling policy.
*/
public onAlarm(action: IAlarmAction) {
public onAlarm(...actions: IAlarmAction[]) {
if (this.alarmActions === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe this can happen, since it's a variadic method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're thinking of the parameter. This is about the instance member.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh 🤦🏻‍♂️

this.alarmActions = [];
}

this.alarmActions.push(action.alarmActionArn);
this.alarmActions.push(...actions.map(a => a.alarmActionArn));
}

/**
* Trigger this action if there is insufficient data to evaluate the alarm
*
* Typically the ARN of an SNS topic or ARN of an AutoScaling policy.
*/
public onInsufficientData(action: IAlarmAction) {
public onInsufficientData(...actions: IAlarmAction[]) {
if (this.insufficientDataActions === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

this.insufficientDataActions = [];
}

this.insufficientDataActions.push(action.alarmActionArn);
this.insufficientDataActions.push(...actions.map(a => a.alarmActionArn));
}

/**
* Trigger this action if the alarm returns from breaching state into ok state
*
* Typically the ARN of an SNS topic or ARN of an AutoScaling policy.
*/
public onOk(action: IAlarmAction) {
public onOk(...actions: IAlarmAction[]) {
if (this.okActions === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

And also here

this.okActions = [];
}

this.okActions.push(action.alarmActionArn);
this.okActions.push(...actions.map(a => a.alarmActionArn));
}

/**
Expand Down
15 changes: 14 additions & 1 deletion packages/@aws-cdk/cloudwatch/lib/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ export class Dashboard extends Construct {
constructor(parent: Construct, name: string, props?: DashboardProps) {
super(parent, name);

// WORKAROUND -- Dashboard cannot be updated if the DashboardName is missing.
// This is a bug in CloudFormation, but we don't want CDK users to have a bad
// experience. We'll generate a name here if you did not supply one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's open an issue to track the fix and revert the workaround (and reference from here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

const dashboardName = (props && props.dashboardName) || this.generateDashboardName();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not make dashboard naming mandatory? It's one of those things that are expressly targeting human interactions, so I think it's reasonable to target an optimum console experience here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered that as well, but the rationale is to not make it required for all intermediate constructs that might contain the dashboard.

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case I would expect the Construct to have a dashboardName input of some sort. But okay I get your point.


new cloudwatch.DashboardResource(this, 'Resource', {
dashboardName: props && props.dashboardName,
dashboardName,
dashboardBody: new Token(() => {
const column = new Column(...this.rows);
column.position(0, 0);
Expand All @@ -48,4 +53,12 @@ export class Dashboard extends Construct {
const w = widgets.length > 1 ? new Row(...widgets) : widgets[0];
this.rows.push(w);
}

/**
* Generate a unique dashboard name in case the user didn't supply one
*/
private generateDashboardName(): string {
// This will include the Stack name and is hence unique
return this.path.replace('/', '-');
Copy link
Contributor

Choose a reason for hiding this comment

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

Do dashboard names have a length limit? Path can be quite long. I would use the the logical ID we generate for the resource prefixed by the stack name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't. That id is only available after the DashboardResource is created, at which point the dashboardName is locked in.

Copy link
Contributor

Choose a reason for hiding this comment

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

Token?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, of course. It's going to be ugly, but I guess...

Copy link
Contributor

Choose a reason for hiding this comment

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

Above comment about making the name required in props stands even more true here as:

  1. This will not be a console-friendly string
  2. This might very well be longer than the maximum dashboard name size (255 characters)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might be. Unlikely though, and in that case you can still pass a proper name.

Copy link
Contributor

Choose a reason for hiding this comment

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

Unless someone hid it somewhere in their construct and gave you no way to name it yourself?

}
}
6 changes: 3 additions & 3 deletions packages/@aws-cdk/cloudwatch/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ export class GraphWidget extends ConcreteWidget {
view: 'timeSeries',
title: this.props.title,
region: this.props.region || new AwsRegion(),
metrics: (this.props.left || []).map(m => m.toGraphJson('left')).concat(
(this.props.right || []).map(m => m.toGraphJson('right'))),
metrics: (this.props.left || []).map(m => m.graphJson('left')).concat(
(this.props.right || []).map(m => m.graphJson('right'))),
annotations: {
horizontal: (this.props.leftAnnotations || []).map(mapAnnotation('left')).concat(
(this.props.rightAnnotations || []).map(mapAnnotation('right')))
Expand Down Expand Up @@ -203,7 +203,7 @@ export class SingleValueWidget extends ConcreteWidget {
view: 'singleValue',
title: this.props.title,
region: this.props.region || new AwsRegion(),
metrics: this.props.metrics.map(m => m.toGraphJson('left'))
metrics: this.props.metrics.map(m => m.graphJson('left'))
}
}];
}
Expand Down
23 changes: 16 additions & 7 deletions packages/@aws-cdk/cloudwatch/lib/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,22 @@ export class Metric {
this.metricName = props.metricName;
this.periodSec = props.periodSec !== undefined ? props.periodSec : 300;
this.statistic = props.statistic || "Average";
this.label = props.label;
this.color = props.color;
this.unit = props.unit;

// Try parsing, this will throw if it's not a valid stat
parseStatistic(this.statistic);
}

/**
* Return a copy of Metric with properties changed
* @param props Re
* Return a copy of Metric with properties changed.
*
* All properties except namespace and metricName can be changed.
*
* @param props The set of properties to change.
*/
public with(props: ChangeMetricProps): Metric {
public with(props: MetricCustomizations): Metric {
return new Metric({
dimensions: ifUndefined(props.dimensions, this.dimensions),
namespace: this.namespace,
Expand Down Expand Up @@ -149,8 +154,10 @@ export class Metric {

/**
* Return the JSON structure which represents this metric in an alarm
*
* This will be called by Alarm, no need for clients to call this.
*/
public toAlarmJson(): MetricAlarmJson {
public alarmInfo(): AlarmMetricInfo {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just AlarmMetric? Whenever I see "Info" in the name of a class I die a little from the inside.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know.

The reason I'm dancing around it is because it's a fraction of the structure required to represent an Alarm. An AlarmMetric is not really a concept that we have, this is half of the properties of AlarmResourceProps.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does it have to be a public API? If there's a way to hide it, name is not that critical.

Another option is to move this logic be part of Alarm instead of the Metric. The metric should be able to expose all this info for the Alarm to be able to use it (and not vice versa).

const stat = parseStatistic(this.statistic);

return {
Expand All @@ -166,8 +173,10 @@ export class Metric {

/**
* Return the JSON structure which represents this metric in a graph
*
* This will be called by GraphWidget, no need for clients to call this.
*/
public toGraphJson(yAxis: string): any[] {
public graphJson(yAxis: string): any[] {
// Namespace and metric Name
const ret: any[] = [
this.namespace,
Expand Down Expand Up @@ -196,7 +205,7 @@ export class Metric {
/**
* Properties used to construct the Metric identifying part of an Alarm
*/
export interface MetricAlarmJson {
export interface AlarmMetricInfo {
/**
* The dimensions to apply to the alarm
*/
Expand Down Expand Up @@ -295,7 +304,7 @@ export enum Unit {
/**
* Properties of a metric that can be changed
*/
export interface ChangeMetricProps {
export interface MetricCustomizations {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd use singular form MetricCustomization

/**
* Dimensions of the metric
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"DashCCD7F836": {
"Type": "AWS::CloudWatch::Dashboard",
"Properties": {
"DashboardName": "aws-cdk-cloudwatch-Dash",
"DashboardBody": {
"Fn::Sub": [
"{\"widgets\":[{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"# This is my dashboard\"}},{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":6,\"y\":0,\"properties\":{\"markdown\":\"you like?\"}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":2,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Messages in queue\",\"region\":\"${ref0}\",\"annotations\":{\"alarms\":[\"${ref1}\"]},\"yAxis\":{\"left\":{\"min\":0}}}},{\"type\":\"metric\",\"width\":6,\"height\":6,\"x\":0,\"y\":8,\"properties\":{\"view\":\"timeSeries\",\"title\":\"More messages in queue with alarm annotation\",\"region\":\"${ref0}\",\"metrics\":[[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"${ref2}\",{\"yAxis\":\"left\",\"period\":300,\"stat\":\"Average\"}]],\"annotations\":{\"horizontal\":[{\"label\":\"ApproximateNumberOfMessagesVisible >= 100 for 3 datapoints within 15 minutes\",\"value\":100,\"yAxis\":\"left\"}]},\"yAxis\":{\"left\":{\"min\":0},\"right\":{\"min\":0}}}},{\"type\":\"metric\",\"width\":6,\"height\":3,\"x\":0,\"y\":14,\"properties\":{\"view\":\"singleValue\",\"title\":\"Current messages in queue\",\"region\":\"${ref0}\",\"metrics\":[[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"${ref2}\",{\"yAxis\":\"left\",\"period\":300,\"stat\":\"Average\"}]]}}]}",
Expand Down
18 changes: 17 additions & 1 deletion packages/@aws-cdk/cloudwatch/test/test.dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, haveResource, isSuperObject } from '@aws-cdk/assert';
import { Stack } from '@aws-cdk/core';
import { App, Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { Dashboard, GraphWidget, TextWidget } from '../lib';

Expand Down Expand Up @@ -94,6 +94,22 @@ export = {

test.done();
},

'work around CloudFormation bug'(test: Test) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Make sure issue is referenced here as well

// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');

// WHEN
new Dashboard(stack, 'MyDashboard');

// THEN
expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', {
DashboardName: 'MyStack-MyDashboard'
}));

test.done();
}
};

/**
Expand Down
25 changes: 25 additions & 0 deletions packages/@aws-cdk/cloudwatch/test/test.graphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@ export = {
test.done();
},

'label and color are respected in constructor'(test: Test) {
// WHEN
const widget = new GraphWidget({
left: [new Metric({ namespace: 'CDK', metricName: 'Test', label: 'MyMetric', color: '000000' }) ],
});

// THEN
test.deepEqual(resolve(widget.toJson()), [{
type: 'metric',
width: 6,
height: 6,
properties: {
view: 'timeSeries',
region: { Ref: 'AWS::Region' },
metrics: [
['CDK', 'Test', { yAxis: 'left', period: 300, stat: 'Average', label: 'MyMetric', color: '000000' }],
],
annotations: { horizontal: [] },
yAxis: { left: { min: 0 }, right: { min: 0 } }
}
}]);

test.done();
},

'singlevalue widget'(test: Test) {
// GIVEN
const metric = new Metric({ namespace: 'CDK', metricName: 'Test' });
Expand Down
22 changes: 11 additions & 11 deletions packages/@aws-cdk/lambda/lib/lambda-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeMetricProps, Metric } from '@aws-cdk/cloudwatch';
import { Metric, MetricCustomizations } from '@aws-cdk/cloudwatch';
import { AccountPrincipal, Arn, Construct, FnSelect, FnSplit, PolicyPrincipal,
PolicyStatement, resolve, ServicePrincipal, Token } from '@aws-cdk/core';
import { EventRuleTarget, IEventRuleTarget } from '@aws-cdk/events';
Expand Down Expand Up @@ -42,7 +42,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
/**
* Return the given named metric for this Lambda
*/
public static metricAll(metricName: string, props?: ChangeMetricProps): Metric {
public static metricAll(metricName: string, props?: MetricCustomizations): Metric {
return new Metric({
namespace: 'AWS/Lambda',
metricName,
Expand All @@ -54,7 +54,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public static metricAllErrors(props?: ChangeMetricProps): Metric {
public static metricAllErrors(props?: MetricCustomizations): Metric {
return LambdaRef.metricAll('Errors', { statistic: 'sum', ...props });
}

Expand All @@ -63,7 +63,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default average over 5 minutes
*/
public static metricAllDuration(props?: ChangeMetricProps): Metric {
public static metricAllDuration(props?: MetricCustomizations): Metric {
return LambdaRef.metricAll('Duration', props);
}

Expand All @@ -72,7 +72,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public static metricAllInvocations(props?: ChangeMetricProps): Metric {
public static metricAllInvocations(props?: MetricCustomizations): Metric {
return LambdaRef.metricAll('Invocations', { statistic: 'sum', ...props });
}

Expand All @@ -81,7 +81,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public static metricAllThrottles(props?: ChangeMetricProps): Metric {
public static metricAllThrottles(props?: MetricCustomizations): Metric {
return LambdaRef.metricAll('Throttles', { statistic: 'sum', ...props });
}

Expand Down Expand Up @@ -167,7 +167,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
/**
* Return the given named metric for this Lambda
*/
public metric(metricName: string, props?: ChangeMetricProps): Metric {
public metric(metricName: string, props?: MetricCustomizations): Metric {
return new Metric({
namespace: 'AWS/Lambda',
metricName,
Expand All @@ -181,7 +181,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public metricErrors(props?: ChangeMetricProps): Metric {
public metricErrors(props?: MetricCustomizations): Metric {
return this.metric('Errors', { statistic: 'sum', ...props });
}

Expand All @@ -190,7 +190,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default average over 5 minutes
*/
public metricDuration(props?: ChangeMetricProps): Metric {
public metricDuration(props?: MetricCustomizations): Metric {
return this.metric('Duration', props);
}

Expand All @@ -199,7 +199,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public metricInvocations(props?: ChangeMetricProps): Metric {
public metricInvocations(props?: MetricCustomizations): Metric {
return this.metric('Invocations', { statistic: 'sum', ...props });
}

Expand All @@ -208,7 +208,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
*
* @default sum over 5 minutes
*/
public metricThrottles(props?: ChangeMetricProps): Metric {
public metricThrottles(props?: MetricCustomizations): Metric {
return this.metric('Throttles', { statistic: 'sum', ...props });
}

Expand Down