Skip to content

Commit 6db09b0

Browse files
authored
feat(dashboards): improve customizability (#349)
# Change Info This change allows for improved customization of dashboards. It will allow consumers of this package to control how many dashboards they generate, the configuration of those dashboards, and the widgets on those dashboards. This probably helps address this related github issue: #66. This change is backwards compatible and consumers must make explicit changes to their code if they would like to take advantage of the additional dynamism around dashboard generation this contribution provides. As a consequence of this, the dashboard configuration provided by this contribution is not as robust or as flexible as I would like it to be, but this comes at the trade-off of being minimally intrusive to the existing codebase. # Testing Done I've used my own CDK application to test this and have verified that these new changes allow me to create a custom number of dashboards according to my own configuration. # Backwards Compatibility This change was made backwards compatible by retrofitting the existing `DefaultDashboardFactory` to also be an implementation of `IDynamicDashboardFactory` with an opinionated implementation which creates three dashboards: Summary, Details, and Alarms. ## Backwards Compatibility Testing Done I also tested this for backwards compatibility by: 1. Using my own CDK application by using the existing constructs exposed by the `MonitoringFacade` and verified that `monitoring.addSegment` and `monitoring.monitorXYZ` calls populated the default dashboards as expected 2. Verifying that the jest snapshots for the `DefaultDashboardFactory` did not change. # How to consume / Documentation I've added a README section on how to consume these changes. # Limitations of this approach 1. This does not really support minor-changes to existing dashboard content. If you want to use this, it amounts to a full swap & replace. Re-arrangement of existing widgets to new dashboards can easily be done, but small modifications to existing widgets are not cheap with this change. 3. This change introduces some coupling between dashboard types and IDynamicDashboardSegments: when creating IDynamicDashboardSegment implementations, you are required to know which dashboards you are generating. However, since this contribution is minimally intrusive, I thought the above were decent trade-offs to form the basis of PR discussions. # Forward Thinking / Additional Augmentations Organizations can use this to configure their own defaults for service teams in their organization and setup best practices which can easily be consumed by service teams when creating their dashboards. Further, such organizations can even introduce additional dashboard types (e.g. for higher level operational or business review) which can exist separately from service/engineering dashboards which are intended to have higher detail and be consumed by more technical audiences. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_
1 parent 24713c9 commit 6db09b0

File tree

11 files changed

+1566
-38
lines changed

11 files changed

+1566
-38
lines changed

API.md

Lines changed: 1068 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,79 @@ This is a general procedure on how to do it:
265265

266266
Both of these monitoring base classes are dashboard segments, so you can add them to your monitoring by calling `.addSegment()` on the `MonitoringFacade`.
267267

268+
### Custom dashboards
269+
270+
If you want *even* more flexibility, you can take complete control over dashboard generation by leveraging dynamic dashboarding features. This allows you to create an arbitrary number of dashboards while configuring each of them separately. You can do this in three simple steps:
271+
272+
1. Create a dynamic dashboard factory
273+
2. Create `IDynamicDashboardSegment` implementations
274+
3. Add Dynamic Segments to your `MonitoringFacade`
275+
276+
#### Create a dynamic dashboard factory
277+
278+
The below code sample will generate two dashboards with the following names:
279+
* ExampleDashboards-HostedService
280+
* ExampleDashboards-Infrastructure
281+
282+
283+
```ts
284+
// create the dynamic dashboard factory.
285+
const factory = new DynamicDashboardFactory(stack, "DynamicDashboards", {
286+
dashboardNamePrefix: "ExampleDashboards",
287+
dashboardConfigs: [
288+
// 'name' is the minimum required configuration
289+
{ name: "HostedService" },
290+
// below is an example of additional dashboard-specific config options
291+
{
292+
name: "Infrastructure",
293+
range: Duration.hours(3),
294+
periodOverride: PeriodOverride.AUTO,
295+
renderingPreference: DashboardRenderingPreference.BITMAP_ONLY
296+
},
297+
],
298+
});
299+
```
300+
301+
#### Create `IDynamicDashboardSegment` implementations
302+
For each construct you want monitored, you will need to create an implementation of an `IDynamicDashboardSegment`. The following is a basic reference implementation as an example:
303+
304+
```ts
305+
export enum DashboardTypes {
306+
HostedService = "HostedService",
307+
Infrastructure = "Infrastructure",
308+
}
309+
310+
class ExampleSegment implements IDynamicDashboardSegment {
311+
widgetsForDashboard(name: string): IWidget[] {
312+
// this logic is what's responsible for allowing your dynamic segment to return
313+
// different widgets for different dashboards
314+
switch (name) {
315+
case DashboardTypes.HostedService:
316+
return [new TextWidget({ markdown: "This shows metrics for your service hosted on AWS Infrastructure" })];
317+
case DashboardTypes.Infrastructure:
318+
return [new TextWidget({ markdown: "This shows metrics for the AWS Infrastructure supporting your hosted service" })];
319+
default:
320+
throw new Error("Unexpected dashboard name!");
321+
}
322+
}
323+
}
324+
```
325+
326+
#### Add Dynamic Segments to MonitoringFacade
327+
328+
When you have instances of an `IDynamicDashboardSegment` to use, they can be added to your dashboard like this:
329+
330+
```ts
331+
monitoring.addDynamicSegment(new ExampleSegment());
332+
```
333+
334+
Now, this widget will be added to both dashboards and will show different content depending on the dashboard. Using the above example code, two dashboards will be generated with the following content:
335+
336+
* Dashboard Name: "ExampleDashboards-HostedService"
337+
* Content: "This shows metrics for your service hosted on AWS Infrastructure"
338+
* Dashboard Name: "ExampleDashboards-Infrastructure"
339+
* Content: "This shows metrics for the AWS Infrastructure supporting your hosted service"
340+
268341
### Monitoring scopes
269342

270343
You can monitor complete CDK construct scopes using an aspect. It will automatically discover all monitorable resources within the scope recursively and add them to your dashboard.

lib/common/monitoring/Monitoring.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { IWidget } from "aws-cdk-lib/aws-cloudwatch";
22

33
import { MonitoringScope } from "./MonitoringScope";
44
import {
5+
DefaultDashboards,
56
IDashboardSegment,
7+
IDynamicDashboardSegment,
68
MonitoringDashboardsOverrideProps,
79
UserProvidedNames,
810
} from "../../dashboard";
@@ -28,7 +30,9 @@ export interface BaseMonitoringProps
2830
/**
2931
* An independent unit of monitoring. This is the base for all monitoring classes with alarm support.
3032
*/
31-
export abstract class Monitoring implements IDashboardSegment {
33+
export abstract class Monitoring
34+
implements IDashboardSegment, IDynamicDashboardSegment
35+
{
3236
protected readonly scope: MonitoringScope;
3337
protected readonly alarms: AlarmWithAnnotation[];
3438
protected readonly localAlarmNamePrefixOverride?: string;
@@ -101,4 +105,17 @@ export abstract class Monitoring implements IDashboardSegment {
101105
* Returns widgets to be placed on the main dashboard.
102106
*/
103107
abstract widgets(): IWidget[];
108+
109+
widgetsForDashboard(name: string): IWidget[] {
110+
switch (name) {
111+
case DefaultDashboards.SUMMARY:
112+
return this.summaryWidgets();
113+
case DefaultDashboards.DETAIL:
114+
return this.widgets();
115+
case DefaultDashboards.ALARMS:
116+
return this.alarmWidgets();
117+
default:
118+
return [];
119+
}
120+
}
104121
}

lib/dashboard/DefaultDashboardFactory.ts

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
PeriodOverride,
66
} from "aws-cdk-lib/aws-cloudwatch";
77
import { Construct } from "constructs";
8-
98
import { BitmapDashboard } from "./BitmapDashboard";
109
import { DashboardRenderingPreference } from "./DashboardRenderingPreference";
1110
import { DashboardWithBitmapCopy } from "./DashboardWithBitmapCopy";
11+
import { IDynamicDashboardSegment } from "./DynamicDashboardSegment";
1212
import { IDashboardFactory, IDashboardFactoryProps } from "./IDashboardFactory";
13+
import { IDynamicDashboardFactory } from "./IDynamicDashboardFactory";
1314

1415
export interface MonitoringDashboardsProps {
1516
/**
@@ -67,15 +68,25 @@ export interface MonitoringDashboardsProps {
6768
readonly renderingPreference?: DashboardRenderingPreference;
6869
}
6970

71+
export enum DefaultDashboards {
72+
SUMMARY = "Summary",
73+
DETAIL = "Detail",
74+
ALARMS = "Alarms",
75+
}
76+
7077
export class DefaultDashboardFactory
7178
extends Construct
72-
implements IDashboardFactory
79+
implements IDashboardFactory, IDynamicDashboardFactory
7380
{
81+
// Legacy Dashboard Fields
7482
readonly dashboard?: Dashboard;
7583
readonly summaryDashboard?: Dashboard;
7684
readonly alarmDashboard?: Dashboard;
7785
readonly anyDashboardCreated: boolean;
7886

87+
// Dynamic Dashboard Fields
88+
readonly dashboards: Record<string, Dashboard> = {};
89+
7990
constructor(scope: Construct, id: string, props: MonitoringDashboardsProps) {
8091
super(scope, id);
8192

@@ -108,6 +119,7 @@ export class DefaultDashboardFactory
108119
periodOverride:
109120
props.detailDashboardPeriodOverride ?? PeriodOverride.INHERIT,
110121
});
122+
this.dashboards[DefaultDashboards.DETAIL] = this.dashboard;
111123
}
112124
if (createSummaryDashboard) {
113125
anyDashboardCreated = true;
@@ -121,6 +133,7 @@ export class DefaultDashboardFactory
121133
props.summaryDashboardPeriodOverride ?? PeriodOverride.INHERIT,
122134
}
123135
);
136+
this.dashboards[DefaultDashboards.SUMMARY] = this.summaryDashboard;
124137
}
125138
if (createAlarmDashboard) {
126139
anyDashboardCreated = true;
@@ -134,26 +147,12 @@ export class DefaultDashboardFactory
134147
props.detailDashboardPeriodOverride ?? PeriodOverride.INHERIT,
135148
}
136149
);
150+
this.dashboards[DefaultDashboards.ALARMS] = this.alarmDashboard;
137151
}
138152

139153
this.anyDashboardCreated = anyDashboardCreated;
140154
}
141155

142-
protected createDashboard(
143-
renderingPreference: DashboardRenderingPreference,
144-
id: string,
145-
props: DashboardProps
146-
) {
147-
switch (renderingPreference) {
148-
case DashboardRenderingPreference.INTERACTIVE_ONLY:
149-
return new Dashboard(this, id, props);
150-
case DashboardRenderingPreference.BITMAP_ONLY:
151-
return new BitmapDashboard(this, id, props);
152-
case DashboardRenderingPreference.INTERACTIVE_AND_BITMAP:
153-
return new DashboardWithBitmapCopy(this, id, props);
154-
}
155-
}
156-
157156
addSegment(props: IDashboardFactoryProps) {
158157
if ((props.overrideProps?.addToDetailDashboard ?? true) && this.dashboard) {
159158
this.dashboard.addWidgets(...props.segment.widgets());
@@ -172,6 +171,33 @@ export class DefaultDashboardFactory
172171
}
173172
}
174173

174+
addDynamicSegment(segment: IDynamicDashboardSegment): void {
175+
this.dashboard?.addWidgets(
176+
...segment.widgetsForDashboard(DefaultDashboards.DETAIL)
177+
);
178+
this.summaryDashboard?.addWidgets(
179+
...segment.widgetsForDashboard(DefaultDashboards.SUMMARY)
180+
);
181+
this.alarmDashboard?.addWidgets(
182+
...segment.widgetsForDashboard(DefaultDashboards.ALARMS)
183+
);
184+
}
185+
186+
protected createDashboard(
187+
renderingPreference: DashboardRenderingPreference,
188+
id: string,
189+
props: DashboardProps
190+
) {
191+
switch (renderingPreference) {
192+
case DashboardRenderingPreference.INTERACTIVE_ONLY:
193+
return new Dashboard(this, id, props);
194+
case DashboardRenderingPreference.BITMAP_ONLY:
195+
return new BitmapDashboard(this, id, props);
196+
case DashboardRenderingPreference.INTERACTIVE_AND_BITMAP:
197+
return new DashboardWithBitmapCopy(this, id, props);
198+
}
199+
}
200+
175201
createdDashboard(): Dashboard | undefined {
176202
return this.dashboard;
177203
}
@@ -183,4 +209,17 @@ export class DefaultDashboardFactory
183209
createdAlarmDashboard(): Dashboard | undefined {
184210
return this.alarmDashboard;
185211
}
212+
213+
getDashboard(name: string): Dashboard | undefined {
214+
switch (name) {
215+
case DefaultDashboards.SUMMARY:
216+
return this.summaryDashboard;
217+
case DefaultDashboards.DETAIL:
218+
return this.dashboard;
219+
case DefaultDashboards.ALARMS:
220+
return this.alarmDashboard;
221+
default:
222+
throw new Error("Unexpected dashboard name!");
223+
}
224+
}
186225
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Duration } from "aws-cdk-lib";
2+
import {
3+
Dashboard,
4+
DashboardProps,
5+
PeriodOverride,
6+
} from "aws-cdk-lib/aws-cloudwatch";
7+
import { Construct } from "constructs";
8+
9+
import { BitmapDashboard } from "./BitmapDashboard";
10+
import { DashboardRenderingPreference } from "./DashboardRenderingPreference";
11+
import { DashboardWithBitmapCopy } from "./DashboardWithBitmapCopy";
12+
import { DefaultDashboards } from "./DefaultDashboardFactory";
13+
import { IDynamicDashboardSegment } from "./DynamicDashboardSegment";
14+
import { IDynamicDashboardFactory } from "./IDynamicDashboardFactory";
15+
16+
export interface DynamicDashboardConfiguration {
17+
/**
18+
* Name of the dashboard. Full dashboard name will take the form of:
19+
* `{@link MonitoringDynamicDashboardsProps.dashboardNamePrefix}-{@link name}`
20+
*
21+
* NOTE: The dashboard names in {@link DefaultDashboardFactory.DefaultDashboards}
22+
* are reserved and cannot be used as dashboard names.
23+
*/
24+
readonly name: string;
25+
26+
/**
27+
* Dashboard rendering preference.
28+
*
29+
* @default - DashboardRenderingPreference.INTERACTIVE_ONLY
30+
*/
31+
readonly renderingPreference?: DashboardRenderingPreference;
32+
33+
/**
34+
* Range of the dashboard
35+
* @default - 8 hours
36+
*/
37+
readonly range?: Duration;
38+
39+
/**
40+
* Period override for the dashboard.
41+
* @default - respect individual graphs (PeriodOverride.INHERIT)
42+
*/
43+
readonly periodOverride?: PeriodOverride;
44+
}
45+
46+
export interface MonitoringDynamicDashboardsProps {
47+
/**
48+
* Prefix added to each dashboard's name.
49+
* This allows to have all dashboards sorted close to each other and also separate multiple monitoring facades.
50+
*/
51+
readonly dashboardNamePrefix: string;
52+
53+
/**
54+
* List of dashboard types to generate.
55+
*/
56+
readonly dashboardConfigs: DynamicDashboardConfiguration[];
57+
}
58+
59+
export class DynamicDashboardFactory
60+
extends Construct
61+
implements IDynamicDashboardFactory
62+
{
63+
readonly dashboards: Record<string, Dashboard> = {};
64+
65+
constructor(
66+
scope: Construct,
67+
id: string,
68+
props: MonitoringDynamicDashboardsProps
69+
) {
70+
super(scope, id);
71+
72+
props.dashboardConfigs.forEach((dashboardConfig) => {
73+
if (this.dashboards[dashboardConfig.name]) {
74+
throw new Error(
75+
`Duplicate dashboard name found: ${dashboardConfig.name}`
76+
);
77+
}
78+
79+
if (
80+
Object.values<string>(DefaultDashboards).includes(dashboardConfig.name)
81+
) {
82+
throw new Error(
83+
`${dashboardConfig.name} is a reserved name and cannot be used`
84+
);
85+
}
86+
87+
const renderingPreference =
88+
dashboardConfig.renderingPreference ??
89+
DashboardRenderingPreference.INTERACTIVE_ONLY;
90+
const start: string =
91+
"-" + (dashboardConfig.range ?? Duration.hours(8).toIsoString());
92+
93+
const dashboard = this.createDashboard(
94+
renderingPreference,
95+
dashboardConfig.name,
96+
{
97+
dashboardName: `${props.dashboardNamePrefix}-${dashboardConfig.name}`,
98+
start,
99+
periodOverride:
100+
dashboardConfig.periodOverride ?? PeriodOverride.INHERIT,
101+
}
102+
);
103+
104+
this.dashboards[dashboardConfig.name] = dashboard;
105+
});
106+
}
107+
108+
protected createDashboard(
109+
renderingPreference: DashboardRenderingPreference,
110+
id: string,
111+
props: DashboardProps
112+
) {
113+
switch (renderingPreference) {
114+
case DashboardRenderingPreference.INTERACTIVE_ONLY:
115+
return new Dashboard(this, id, props);
116+
case DashboardRenderingPreference.BITMAP_ONLY:
117+
return new BitmapDashboard(this, id, props);
118+
case DashboardRenderingPreference.INTERACTIVE_AND_BITMAP:
119+
return new DashboardWithBitmapCopy(this, id, props);
120+
}
121+
}
122+
123+
addDynamicSegment(segment: IDynamicDashboardSegment): void {
124+
for (const type in this.dashboards) {
125+
const dashboard = this.dashboards[type];
126+
dashboard.addWidgets(...segment.widgetsForDashboard(type));
127+
}
128+
}
129+
130+
getDashboard(type: string): Dashboard | undefined {
131+
return this.dashboards[type];
132+
}
133+
}

0 commit comments

Comments
 (0)