Skip to content

Commit

Permalink
feat(Aurora): Added support for Serverless Clusters
Browse files Browse the repository at this point in the history
Added new AuroraCluster Monitoring with Aurora specific metrics/alarms.
  • Loading branch information
zqumei0 committed Nov 9, 2023
1 parent 84609a4 commit ddf6310
Show file tree
Hide file tree
Showing 14 changed files with 15,141 additions and 10 deletions.
1,224 changes: 1,216 additions & 8 deletions API.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ You can browse the documentation at https://constructs.dev/packages/cdk-monitori
| AWS API Gateway (REST API) (`.monitorApiGateway()`) | TPS, latency, errors | Latency, error count/rate, low/high TPS | To see metrics, you have to enable Advanced Monitoring |
| AWS API Gateway V2 (HTTP API) (`.monitorApiGatewayV2HttpApi()`) | TPS, latency, errors | Latency, error count/rate, low/high TPS | To see route level metrics, you have to enable Advanced Monitoring |
| AWS AppSync (GraphQL API) (`.monitorAppSyncApi()`) | TPS, latency, errors | Latency, error count/rate, low/high TPS | |
| Amazon Aurora (`.monitorAuroraCluster()`) | Query duration, connections, latency, CPU usage, Serverless Database Capacity | Connections, Serverless Database Capacity and CPU usage | |
| AWS Billing (`.monitorBilling()`) | AWS account cost | Total cost (anomaly) | [Requires enabling](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/gs_monitor_estimated_charges_with_cloudwatch.html#gs_turning_on_billing_metrics) the **Receive Billing Alerts** option in AWS Console / Billing Preferences |
| AWS Certificate Manager (`.monitorCertificate()`) | Certificate expiration | Days until expiration | |
| AWS CloudFront (`.monitorCloudFrontDistribution()`) | TPS, traffic, latency, errors | Error rate, low/high TPS | |
Expand Down
39 changes: 39 additions & 0 deletions lib/common/monitoring/alarms/AuroraAlarmFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ComparisonOperator,
TreatMissingData,
} from "aws-cdk-lib/aws-cloudwatch";

import { AlarmFactory, CustomAlarmThreshold } from "../../alarm";
import { MetricWithAlarmSupport } from "../../metric";

export interface HighServerlessDatabaseCapacityThreshold
extends CustomAlarmThreshold {
readonly maxServerlessDatabaseCapacity: number;
}

export class AuroraAlarmFactory {
protected readonly alarmFactory: AlarmFactory;

constructor(alarmFactory: AlarmFactory) {
this.alarmFactory = alarmFactory;
}

addMaxServerlessDatabaseCapacity(
metric: MetricWithAlarmSupport,
props: HighServerlessDatabaseCapacityThreshold,
disambiguator?: string
) {
return this.alarmFactory.addAlarm(metric, {
treatMissingData:
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
comparisonOperator:
props.comparisonOperatorOverride ??
ComparisonOperator.LESS_THAN_THRESHOLD,
...props,
disambiguator,
threshold: props.maxServerlessDatabaseCapacity,
alarmNameSuffix: "Serverless-Database-Capacity-High",
alarmDescription: "Serverless Database Capacity usage is too high.",
});
}
}
19 changes: 19 additions & 0 deletions lib/common/monitoring/alarms/UsageAlarmFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ export class UsageAlarmFactory {
this.alarmFactory = alarmFactory;
}

addMaxUsageCountAlarm(
metric: MetricWithAlarmSupport,
props: UsageCountThreshold,
disambiguator?: string
) {
return this.alarmFactory.addAlarm(metric, {
treatMissingData:
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
comparisonOperator:
props.comparisonOperatorOverride ??
ComparisonOperator.GREATER_THAN_THRESHOLD,
...props,
disambiguator,
threshold: props.maxUsageCount,
alarmNameSuffix: "Usage-Count",
alarmDescription: "The count is too high.",
});
}

addMinUsageCountAlarm(
percentMetric: MetricWithAlarmSupport,
props: MinUsageCountThreshold,
Expand Down
1 change: 1 addition & 0 deletions lib/common/monitoring/alarms/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./AgeAlarmFactory";
export * from "./AnomalyDetectingAlarmFactory";
export * from "./AuroraAlarmFactory";
export * from "./CustomAlarmFactory";
export * from "./ConnectionAlarmFactory";
export * from "./DynamoAlarmFactory";
Expand Down
2 changes: 2 additions & 0 deletions lib/facade/IMonitoringAspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ApiGatewayMonitoringOptions,
ApiGatewayV2MonitoringOptions,
AppSyncMonitoringOptions,
AuroraClusterMonitoringOptions,
AutoScalingGroupMonitoringOptions,
BillingMonitoringOptions,
CertificateManagerMonitoringOptions,
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface MonitoringAspectProps {
readonly apiGateway?: MonitoringAspectType<ApiGatewayMonitoringOptions>;
readonly apiGatewayV2?: MonitoringAspectType<ApiGatewayV2MonitoringOptions>;
readonly appSync?: MonitoringAspectType<AppSyncMonitoringOptions>;
readonly auroraCluster?: MonitoringAspectType<AuroraClusterMonitoringOptions>;
readonly autoScalingGroup?: MonitoringAspectType<AutoScalingGroupMonitoringOptions>;
readonly billing?: MonitoringAspectType<BillingMonitoringOptions>;
readonly cloudFront?: MonitoringAspectType<CloudFrontDistributionMonitoringOptions>;
Expand Down
14 changes: 14 additions & 0 deletions lib/facade/MonitoringAspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class MonitoringAspect implements IAspect {
this.monitorApiGateway(node);
this.monitorApiGatewayV2(node);
this.monitorAppSync(node);
this.monitorAuroraCluster(node);
this.monitorAutoScalingGroup(node);
this.monitorCloudFront(node);
this.monitorCodeBuild(node);
Expand Down Expand Up @@ -135,6 +136,19 @@ export class MonitoringAspect implements IAspect {
}
}

private monitorAuroraCluster(node: IConstruct) {
const [isEnabled, props] = this.getMonitoringDetails(
this.props.auroraCluster
);
if (isEnabled && node instanceof rds.ServerlessCluster) {
this.monitoringFacade.monitorAuroraCluster({
cluster: node,
alarmFriendlyName: node.node.path,
...props,
});
}
}

private monitorAutoScalingGroup(node: IConstruct) {
const [isEnabled, props] = this.getMonitoringDetails(
this.props.autoScalingGroup
Expand Down
8 changes: 8 additions & 0 deletions lib/facade/MonitoringFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
ApiGatewayV2HttpApiMonitoringProps,
AppSyncMonitoring,
AppSyncMonitoringProps,
AuroraClusterMonitoring,
AuroraClusterMonitoringProps,
AutoScalingGroupMonitoring,
AutoScalingGroupMonitoringProps,
BillingMonitoring,
Expand Down Expand Up @@ -415,6 +417,12 @@ export class MonitoringFacade extends MonitoringScope {
return this;
}

monitorAuroraCluster(props: AuroraClusterMonitoringProps) {
const segment = new AuroraClusterMonitoring(this, props);
this.addSegment(segment, props);
return this;
}

monitorCertificate(props: CertificateManagerMonitoringProps) {
const segment = new CertificateManagerMonitoring(this, props);
this.addSegment(segment, props);
Expand Down
239 changes: 239 additions & 0 deletions lib/monitoring/aws-rds/AuroraClusterMonitoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import {
GraphWidget,
HorizontalAnnotation,
IWidget,
} from "aws-cdk-lib/aws-cloudwatch";

import {
RdsClusterMetricFactory,
RdsClusterMetricFactoryProps,
} from "./RdsClusterMetricFactory";
import {
BaseMonitoringProps,
ConnectionAlarmFactory,
CountAxisFromZero,
DefaultGraphWidgetHeight,
HalfWidth,
HighConnectionCountThreshold,
LowConnectionCountThreshold,
MetricWithAlarmSupport,
Monitoring,
MonitoringScope,
PercentageAxisFromZeroToHundred,
QuarterWidth,
TimeAxisMillisFromZero,
UsageAlarmFactory,
UsageCountThreshold,
UsageThreshold,
} from "../../common";
import {
MonitoringHeaderWidget,
MonitoringNamingStrategy,
} from "../../dashboard";

export interface AuroraClusterMonitoringOptions extends BaseMonitoringProps {
readonly addMaxServerlessDatabaseCapacityAlarm?: Record<
string,
UsageCountThreshold
>;
readonly addCpuUsageAlarm?: Record<string, UsageThreshold>;
readonly addMinConnectionCountAlarm?: Record<
string,
LowConnectionCountThreshold
>;
readonly addMaxConnectionCountAlarm?: Record<
string,
HighConnectionCountThreshold
>;
}

export interface AuroraClusterMonitoringProps
extends RdsClusterMetricFactoryProps,
AuroraClusterMonitoringOptions {}

export class AuroraClusterMonitoring extends Monitoring {
readonly title: string;
readonly url?: string;

readonly usageAlarmFactory: UsageAlarmFactory;
readonly connectionAlarmFactory: ConnectionAlarmFactory;

readonly usageAnnotations: HorizontalAnnotation[];
readonly connectionAnnotations: HorizontalAnnotation[];

readonly connectionsMetric: MetricWithAlarmSupport;
readonly serverlessDatabaseCapacityMetric: MetricWithAlarmSupport;
readonly cpuUsageMetric: MetricWithAlarmSupport;
readonly selectLatencyMetric: MetricWithAlarmSupport;
readonly insertLatencyMetric: MetricWithAlarmSupport;
readonly updateLatencyMetric: MetricWithAlarmSupport;
readonly deleteLatencyMetric: MetricWithAlarmSupport;
readonly commitLatencyMetric: MetricWithAlarmSupport;

constructor(scope: MonitoringScope, props: AuroraClusterMonitoringProps) {
super(scope, props);

const metricFactory = new RdsClusterMetricFactory(
scope.createMetricFactory(),
props
);
this.connectionsMetric = metricFactory.metricTotalConnectionCount();
this.serverlessDatabaseCapacityMetric =
metricFactory.metricServerlessDatabaseCapacity();
this.cpuUsageMetric = metricFactory.metricAverageCpuUsageInPercent();
this.selectLatencyMetric = metricFactory.metricSelectLatencyP90InMillis();
this.insertLatencyMetric = metricFactory.metricInsertLatencyP90InMillis();
this.updateLatencyMetric = metricFactory.metricUpdateLatencyP90InMillis();
this.deleteLatencyMetric = metricFactory.metricDeleteLatencyP90InMillis();
this.commitLatencyMetric = metricFactory.metricCommitLatencyP90InMillis();

const namingStrategy = new MonitoringNamingStrategy({
...props,
fallbackConstructName: metricFactory.clusterIdentifier,
namedConstruct: props.cluster,
});
this.title = namingStrategy.resolveHumanReadableName();
this.url = scope
.createAwsConsoleUrlFactory()
.getRdsClusterUrl(metricFactory.clusterIdentifier);
const alarmFactory = this.createAlarmFactory(
namingStrategy.resolveAlarmFriendlyName()
);
this.usageAlarmFactory = new UsageAlarmFactory(alarmFactory);
this.connectionAlarmFactory = new ConnectionAlarmFactory(alarmFactory);

this.usageAnnotations = [];
this.connectionAnnotations = [];

for (const disambiguator in props.addCpuUsageAlarm) {
const alarmProps = props.addCpuUsageAlarm[disambiguator];
const createdAlarm = this.usageAlarmFactory.addMaxCpuUsagePercentAlarm(
this.cpuUsageMetric,
alarmProps,
disambiguator
);
this.usageAnnotations.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

for (const disambiguator in props.addMinConnectionCountAlarm) {
const alarmProps = props.addMinConnectionCountAlarm[disambiguator];
const createdAlarm =
this.connectionAlarmFactory.addMinConnectionCountAlarm(
this.connectionsMetric,
alarmProps,
disambiguator
);
this.connectionAnnotations.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

for (const disambiguator in props.addMaxConnectionCountAlarm) {
const alarmProps = props.addMaxConnectionCountAlarm[disambiguator];
const createdAlarm =
this.connectionAlarmFactory.addMaxConnectionCountAlarm(
this.connectionsMetric,
alarmProps,
disambiguator
);
this.connectionAnnotations.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

for (const disambiguator in props.addMaxServerlessDatabaseCapacityAlarm) {
const alarmProps =
props.addMaxServerlessDatabaseCapacityAlarm[disambiguator];
const createdAlarm = this.usageAlarmFactory.addMaxUsageCountAlarm(
this.serverlessDatabaseCapacityMetric,
alarmProps,
disambiguator
);
this.connectionAnnotations.push(createdAlarm.annotation);
this.addAlarm(createdAlarm);
}

props.useCreatedAlarms?.consume(this.createdAlarms());
}

summaryWidgets(): IWidget[] {
return [
this.createTitleWidget(),
this.createCpuUsageWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createConnectionsWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createLatencyWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createServerlessDatabaseCapacityWidget(
QuarterWidth,
DefaultGraphWidgetHeight
),
];
}

widgets(): IWidget[] {
return [
this.createTitleWidget(),
this.createCpuUsageWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createConnectionsWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createLatencyWidget(HalfWidth, DefaultGraphWidgetHeight),
this.createServerlessDatabaseCapacityWidget(
HalfWidth,
DefaultGraphWidgetHeight
),
];
}

createTitleWidget() {
return new MonitoringHeaderWidget({
family: "Aurora Cluster",
title: this.title,
goToLinkUrl: this.url,
});
}

createCpuUsageWidget(width: number, height: number) {
return new GraphWidget({
width,
height,
title: "CPU Usage",
left: [this.cpuUsageMetric],
leftYAxis: PercentageAxisFromZeroToHundred,
leftAnnotations: this.usageAnnotations,
});
}

createConnectionsWidget(width: number, height: number) {
return new GraphWidget({
width,
height,
title: "Connections",
left: [this.connectionsMetric],
leftYAxis: CountAxisFromZero,
leftAnnotations: this.connectionAnnotations,
});
}

createLatencyWidget(width: number, height: number) {
return new GraphWidget({
width,
height,
title: "Query Duration",
left: [
this.selectLatencyMetric,
this.insertLatencyMetric,
this.updateLatencyMetric,
this.deleteLatencyMetric,
this.commitLatencyMetric,
],
leftYAxis: TimeAxisMillisFromZero,
});
}

createServerlessDatabaseCapacityWidget(width: number, height: number) {
return new GraphWidget({
width,
height,
title: "Serverless Database Capacity",
left: [this.serverlessDatabaseCapacityMetric],
leftYAxis: CountAxisFromZero,
});
}
}
Loading

0 comments on commit ddf6310

Please sign in to comment.