From 7460d4ec415b43146e4aab08950e7b60986cbb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Hord=C4=9Bj=C4=8Duk?= Date: Tue, 15 Mar 2022 22:36:12 +0100 Subject: [PATCH] feat: adding monitoring support for CloudWatch Canary (#75) --- API.md | 665 +++++++++++++++++- README.md | 1 + lib/facade/MonitoringAspect.ts | 18 +- lib/facade/MonitoringFacade.ts | 8 + lib/facade/aspect-types.ts | 2 + .../SyntheticsCanaryMetricFactory.ts | 101 +++ .../SyntheticsCanaryMonitoring.ts | 211 ++++++ lib/monitoring/aws-synthetics/index.ts | 2 + lib/monitoring/index.ts | 1 + test/facade/MonitoringAspect.test.ts | 24 +- .../MonitoringAspect.test.ts.snap | 278 ++++++++ .../SyntheticsCanaryMonitoring.test.ts | 78 ++ .../SyntheticsCanaryMonitoring.test.ts.snap | 459 ++++++++++++ 13 files changed, 1844 insertions(+), 4 deletions(-) create mode 100644 lib/monitoring/aws-synthetics/SyntheticsCanaryMetricFactory.ts create mode 100644 lib/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.ts create mode 100644 lib/monitoring/aws-synthetics/index.ts create mode 100644 test/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.test.ts create mode 100644 test/monitoring/aws-synthetics/__snapshots__/SyntheticsCanaryMonitoring.test.ts.snap diff --git a/API.md b/API.md index 429ffba1..53799701 100644 --- a/API.md +++ b/API.md @@ -749,6 +749,7 @@ new MonitoringFacade(scope: Construct, id: string, props: MonitoringFacadeProps) | monitorStepFunctionActivity | *No description.* | | monitorStepFunctionLambdaIntegration | *No description.* | | monitorStepFunctionServiceIntegration | *No description.* | +| monitorSyntheticsCanary | *No description.* | --- @@ -1568,6 +1569,18 @@ public monitorStepFunctionServiceIntegration(props: StepFunctionServiceIntegrati --- +##### `monitorSyntheticsCanary` + +```typescript +public monitorSyntheticsCanary(props: SyntheticsCanaryMonitoringProps): MonitoringFacade +``` + +###### `props`Required + +- *Type:* SyntheticsCanaryMonitoringProps + +--- + #### Static Functions | **Name** | **Description** | @@ -22340,6 +22353,7 @@ const monitoringAspectProps: MonitoringAspectProps = { ... } | sns | MonitoringAspectType | *No description.* | | sqs | MonitoringAspectType | *No description.* | | stepFunctions | MonitoringAspectType | *No description.* | +| syntheticsCanaries | MonitoringAspectType | *No description.* | --- @@ -22583,6 +22597,16 @@ public readonly stepFunctions: MonitoringAspectType; --- +##### `syntheticsCanaries`Optional + +```typescript +public readonly syntheticsCanaries: MonitoringAspectType; +``` + +- *Type:* MonitoringAspectType + +--- + ### MonitoringAspectType #### Initializer @@ -31121,6 +31145,429 @@ public readonly addTimedOutServiceIntegrationsCountAlarm: {[ key: string ]: Erro --- +### SyntheticsCanaryMetricFactoryProps + +#### Initializer + +```typescript +import { SyntheticsCanaryMetricFactoryProps } from 'cdk-monitoring-constructs' + +const syntheticsCanaryMetricFactoryProps: SyntheticsCanaryMetricFactoryProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| canary | monocdk.aws_synthetics.Canary | CloudWatch Canary to monitor. | +| rateComputationMethod | RateComputationMethod | Method used to calculate relative rates. | + +--- + +##### `canary`Required + +```typescript +public readonly canary: Canary; +``` + +- *Type:* monocdk.aws_synthetics.Canary + +CloudWatch Canary to monitor. + +--- + +##### `rateComputationMethod`Optional + +```typescript +public readonly rateComputationMethod: RateComputationMethod; +``` + +- *Type:* RateComputationMethod +- *Default:* average + +Method used to calculate relative rates. + +--- + +### SyntheticsCanaryMonitoringOptions + +#### Initializer + +```typescript +import { SyntheticsCanaryMonitoringOptions } from 'cdk-monitoring-constructs' + +const syntheticsCanaryMonitoringOptions: SyntheticsCanaryMonitoringOptions = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| alarmFriendlyName | string | Plain name, used in naming alarms. | +| humanReadableName | string | Human-readable name is a freeform string, used as a caption or description. | +| localAlarmNamePrefixOverride | string | If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. | +| addToAlarmDashboard | boolean | Flag indicating if the widgets should be added to alarm dashboard. | +| addToDetailDashboard | boolean | Flag indicating if the widgets should be added to detailed dashboard. | +| addToSummaryDashboard | boolean | Flag indicating if the widgets should be added to summary dashboard. | +| useCreatedAlarms | IAlarmConsumer | Calls provided function to process all alarms created. | +| add4xxErrorCountAlarm | {[ key: string ]: ErrorCountThreshold} | *No description.* | +| add4xxErrorRateAlarm | {[ key: string ]: ErrorRateThreshold} | *No description.* | +| add5xxFaultCountAlarm | {[ key: string ]: ErrorCountThreshold} | *No description.* | +| add5xxFaultRateAlarm | {[ key: string ]: ErrorRateThreshold} | *No description.* | +| addAverageLatencyAlarm | {[ key: string ]: LatencyThreshold} | *No description.* | + +--- + +##### `alarmFriendlyName`Optional + +```typescript +public readonly alarmFriendlyName: string; +``` + +- *Type:* string +- *Default:* derives name from the construct itself + +Plain name, used in naming alarms. + +This unique among other resources, and respect the AWS CDK restriction posed on alarm names. The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +--- + +##### `humanReadableName`Optional + +```typescript +public readonly humanReadableName: string; +``` + +- *Type:* string +- *Default:* use alarmFriendlyName + +Human-readable name is a freeform string, used as a caption or description. + +There are no limitations on what it can be. + +--- + +##### `localAlarmNamePrefixOverride`Optional + +```typescript +public readonly localAlarmNamePrefixOverride: string; +``` + +- *Type:* string + +If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. + +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +> [AlarmNamingStrategy for more details on alarm name prefixes](AlarmNamingStrategy for more details on alarm name prefixes) + +--- + +##### `addToAlarmDashboard`Optional + +```typescript +public readonly addToAlarmDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to alarm dashboard. + +--- + +##### `addToDetailDashboard`Optional + +```typescript +public readonly addToDetailDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to detailed dashboard. + +--- + +##### `addToSummaryDashboard`Optional + +```typescript +public readonly addToSummaryDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to summary dashboard. + +--- + +##### `useCreatedAlarms`Optional + +```typescript +public readonly useCreatedAlarms: IAlarmConsumer; +``` + +- *Type:* IAlarmConsumer + +Calls provided function to process all alarms created. + +--- + +##### `add4xxErrorCountAlarm`Optional + +```typescript +public readonly add4xxErrorCountAlarm: {[ key: string ]: ErrorCountThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorCountThreshold} + +--- + +##### `add4xxErrorRateAlarm`Optional + +```typescript +public readonly add4xxErrorRateAlarm: {[ key: string ]: ErrorRateThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorRateThreshold} + +--- + +##### `add5xxFaultCountAlarm`Optional + +```typescript +public readonly add5xxFaultCountAlarm: {[ key: string ]: ErrorCountThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorCountThreshold} + +--- + +##### `add5xxFaultRateAlarm`Optional + +```typescript +public readonly add5xxFaultRateAlarm: {[ key: string ]: ErrorRateThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorRateThreshold} + +--- + +##### `addAverageLatencyAlarm`Optional + +```typescript +public readonly addAverageLatencyAlarm: {[ key: string ]: LatencyThreshold}; +``` + +- *Type:* {[ key: string ]: LatencyThreshold} + +--- + +### SyntheticsCanaryMonitoringProps + +#### Initializer + +```typescript +import { SyntheticsCanaryMonitoringProps } from 'cdk-monitoring-constructs' + +const syntheticsCanaryMonitoringProps: SyntheticsCanaryMonitoringProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| canary | monocdk.aws_synthetics.Canary | CloudWatch Canary to monitor. | +| rateComputationMethod | RateComputationMethod | Method used to calculate relative rates. | +| alarmFriendlyName | string | Plain name, used in naming alarms. | +| humanReadableName | string | Human-readable name is a freeform string, used as a caption or description. | +| localAlarmNamePrefixOverride | string | If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. | +| addToAlarmDashboard | boolean | Flag indicating if the widgets should be added to alarm dashboard. | +| addToDetailDashboard | boolean | Flag indicating if the widgets should be added to detailed dashboard. | +| addToSummaryDashboard | boolean | Flag indicating if the widgets should be added to summary dashboard. | +| useCreatedAlarms | IAlarmConsumer | Calls provided function to process all alarms created. | +| add4xxErrorCountAlarm | {[ key: string ]: ErrorCountThreshold} | *No description.* | +| add4xxErrorRateAlarm | {[ key: string ]: ErrorRateThreshold} | *No description.* | +| add5xxFaultCountAlarm | {[ key: string ]: ErrorCountThreshold} | *No description.* | +| add5xxFaultRateAlarm | {[ key: string ]: ErrorRateThreshold} | *No description.* | +| addAverageLatencyAlarm | {[ key: string ]: LatencyThreshold} | *No description.* | + +--- + +##### `canary`Required + +```typescript +public readonly canary: Canary; +``` + +- *Type:* monocdk.aws_synthetics.Canary + +CloudWatch Canary to monitor. + +--- + +##### `rateComputationMethod`Optional + +```typescript +public readonly rateComputationMethod: RateComputationMethod; +``` + +- *Type:* RateComputationMethod +- *Default:* average + +Method used to calculate relative rates. + +--- + +##### `alarmFriendlyName`Optional + +```typescript +public readonly alarmFriendlyName: string; +``` + +- *Type:* string +- *Default:* derives name from the construct itself + +Plain name, used in naming alarms. + +This unique among other resources, and respect the AWS CDK restriction posed on alarm names. The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +--- + +##### `humanReadableName`Optional + +```typescript +public readonly humanReadableName: string; +``` + +- *Type:* string +- *Default:* use alarmFriendlyName + +Human-readable name is a freeform string, used as a caption or description. + +There are no limitations on what it can be. + +--- + +##### `localAlarmNamePrefixOverride`Optional + +```typescript +public readonly localAlarmNamePrefixOverride: string; +``` + +- *Type:* string + +If this is defined, the local alarm name prefix used in naming alarms for the construct will be set to this value. + +The length must be 1 - 255 characters and although the validation rules are undocumented, we recommend using ASCII and hyphens. + +> [AlarmNamingStrategy for more details on alarm name prefixes](AlarmNamingStrategy for more details on alarm name prefixes) + +--- + +##### `addToAlarmDashboard`Optional + +```typescript +public readonly addToAlarmDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to alarm dashboard. + +--- + +##### `addToDetailDashboard`Optional + +```typescript +public readonly addToDetailDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to detailed dashboard. + +--- + +##### `addToSummaryDashboard`Optional + +```typescript +public readonly addToSummaryDashboard: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Flag indicating if the widgets should be added to summary dashboard. + +--- + +##### `useCreatedAlarms`Optional + +```typescript +public readonly useCreatedAlarms: IAlarmConsumer; +``` + +- *Type:* IAlarmConsumer + +Calls provided function to process all alarms created. + +--- + +##### `add4xxErrorCountAlarm`Optional + +```typescript +public readonly add4xxErrorCountAlarm: {[ key: string ]: ErrorCountThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorCountThreshold} + +--- + +##### `add4xxErrorRateAlarm`Optional + +```typescript +public readonly add4xxErrorRateAlarm: {[ key: string ]: ErrorRateThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorRateThreshold} + +--- + +##### `add5xxFaultCountAlarm`Optional + +```typescript +public readonly add5xxFaultCountAlarm: {[ key: string ]: ErrorCountThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorCountThreshold} + +--- + +##### `add5xxFaultRateAlarm`Optional + +```typescript +public readonly add5xxFaultRateAlarm: {[ key: string ]: ErrorRateThreshold}; +``` + +- *Type:* {[ key: string ]: ErrorRateThreshold} + +--- + +##### `addAverageLatencyAlarm`Optional + +```typescript +public readonly addAverageLatencyAlarm: {[ key: string ]: LatencyThreshold}; +``` + +- *Type:* {[ key: string ]: LatencyThreshold} + +--- + ### ThrottledEventsThreshold #### Initializer @@ -44248,6 +44695,222 @@ Returns widgets to be placed on the main dashboard. +### SyntheticsCanaryMetricFactory + +#### Initializers + +```typescript +import { SyntheticsCanaryMetricFactory } from 'cdk-monitoring-constructs' + +new SyntheticsCanaryMetricFactory(metricFactory: MetricFactory, props: SyntheticsCanaryMetricFactoryProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| metricFactory | MetricFactory | *No description.* | +| props | SyntheticsCanaryMetricFactoryProps | *No description.* | + +--- + +##### `metricFactory`Required + +- *Type:* MetricFactory + +--- + +##### `props`Required + +- *Type:* SyntheticsCanaryMetricFactoryProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| metric4xxErrorCount | *No description.* | +| metric4xxErrorRate | *No description.* | +| metric5xxFaultCount | *No description.* | +| metric5xxFaultRate | *No description.* | +| metricLatencyAverageInMillis | *No description.* | +| metricSuccessInPercent | *No description.* | + +--- + +##### `metric4xxErrorCount` + +```typescript +public metric4xxErrorCount(): Metric | MathExpression +``` + +##### `metric4xxErrorRate` + +```typescript +public metric4xxErrorRate(): Metric | MathExpression +``` + +##### `metric5xxFaultCount` + +```typescript +public metric5xxFaultCount(): Metric | MathExpression +``` + +##### `metric5xxFaultRate` + +```typescript +public metric5xxFaultRate(): Metric | MathExpression +``` + +##### `metricLatencyAverageInMillis` + +```typescript +public metricLatencyAverageInMillis(): Metric | MathExpression +``` + +##### `metricSuccessInPercent` + +```typescript +public metricSuccessInPercent(): Metric | MathExpression +``` + + + + +### SyntheticsCanaryMonitoring + +Monitoring for CloudWatch Synthetics Canaries. + +> [https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html) + +#### Initializers + +```typescript +import { SyntheticsCanaryMonitoring } from 'cdk-monitoring-constructs' + +new SyntheticsCanaryMonitoring(scope: MonitoringScope, props: SyntheticsCanaryMonitoringProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | MonitoringScope | *No description.* | +| props | SyntheticsCanaryMonitoringProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* MonitoringScope + +--- + +##### `props`Required + +- *Type:* SyntheticsCanaryMonitoringProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| addAlarm | Adds an alarm. | +| alarmWidgets | Returns widgets for all alarms. | +| createAlarmFactory | Creates a new alarm factory. | +| createdAlarms | Returns all the alarms created. | +| createMetricFactory | Creates a new metric factory. | +| createWidgetFactory | Creates a new widget factory. | +| summaryWidgets | Returns widgets to be placed on the summary dashboard. | +| widgets | Returns widgets to be placed on the main dashboard. | + +--- + +##### `addAlarm` + +```typescript +public addAlarm(alarm: AlarmWithAnnotation): void +``` + +Adds an alarm. + +###### `alarm`Required + +- *Type:* AlarmWithAnnotation + +alarm to add. + +--- + +##### `alarmWidgets` + +```typescript +public alarmWidgets(): IWidget[] +``` + +Returns widgets for all alarms. + +These can go to runbook or to service dashboard. + +##### `createAlarmFactory` + +```typescript +public createAlarmFactory(alarmNamePrefix: string): AlarmFactory +``` + +Creates a new alarm factory. + +Alarms created will be named with the given prefix, unless a local name override is present. + +###### `alarmNamePrefix`Required + +- *Type:* string + +alarm name prefix. + +--- + +##### `createdAlarms` + +```typescript +public createdAlarms(): AlarmWithAnnotation[] +``` + +Returns all the alarms created. + +##### `createMetricFactory` + +```typescript +public createMetricFactory(): MetricFactory +``` + +Creates a new metric factory. + +##### `createWidgetFactory` + +```typescript +public createWidgetFactory(): IWidgetFactory +``` + +Creates a new widget factory. + +##### `summaryWidgets` + +```typescript +public summaryWidgets(): IWidget[] +``` + +Returns widgets to be placed on the summary dashboard. + +##### `widgets` + +```typescript +public widgets(): IWidget[] +``` + +Returns widgets to be placed on the main dashboard. + + + + ### TaskHealthAlarmFactory #### Initializers @@ -45258,7 +45921,7 @@ Dashboard placement override props. ### IDashboardSegment -- *Implemented By:* ApiGatewayMonitoring, ApiGatewayV2HttpApiMonitoring, AppSyncMonitoring, AutoScalingGroupMonitoring, BillingMonitoring, CertificateManagerMonitoring, CloudFrontDistributionMonitoring, CodeBuildProjectMonitoring, CustomMonitoring, DynamoTableGlobalSecondaryIndexMonitoring, DynamoTableMonitoring, EC2Monitoring, Ec2ServiceMonitoring, ElastiCacheClusterMonitoring, FargateServiceMonitoring, GlueJobMonitoring, KinesisDataAnalyticsMonitoring, KinesisDataStreamMonitoring, KinesisFirehoseMonitoring, LambdaFunctionMonitoring, LogMonitoring, Monitoring, NetworkLoadBalancerMonitoring, OpenSearchClusterMonitoring, RdsClusterMonitoring, RedshiftClusterMonitoring, S3BucketMonitoring, SecretsManagerSecretMonitoring, SingleWidgetDashboardSegment, SnsTopicMonitoring, SqsQueueMonitoring, SqsQueueMonitoringWithDlq, StepFunctionActivityMonitoring, StepFunctionLambdaIntegrationMonitoring, StepFunctionMonitoring, StepFunctionServiceIntegrationMonitoring, IDashboardSegment +- *Implemented By:* ApiGatewayMonitoring, ApiGatewayV2HttpApiMonitoring, AppSyncMonitoring, AutoScalingGroupMonitoring, BillingMonitoring, CertificateManagerMonitoring, CloudFrontDistributionMonitoring, CodeBuildProjectMonitoring, CustomMonitoring, DynamoTableGlobalSecondaryIndexMonitoring, DynamoTableMonitoring, EC2Monitoring, Ec2ServiceMonitoring, ElastiCacheClusterMonitoring, FargateServiceMonitoring, GlueJobMonitoring, KinesisDataAnalyticsMonitoring, KinesisDataStreamMonitoring, KinesisFirehoseMonitoring, LambdaFunctionMonitoring, LogMonitoring, Monitoring, NetworkLoadBalancerMonitoring, OpenSearchClusterMonitoring, RdsClusterMonitoring, RedshiftClusterMonitoring, S3BucketMonitoring, SecretsManagerSecretMonitoring, SingleWidgetDashboardSegment, SnsTopicMonitoring, SqsQueueMonitoring, SqsQueueMonitoringWithDlq, StepFunctionActivityMonitoring, StepFunctionLambdaIntegrationMonitoring, StepFunctionMonitoring, StepFunctionServiceIntegrationMonitoring, SyntheticsCanaryMonitoring, IDashboardSegment #### Methods diff --git a/README.md b/README.md index e88160fa..ea33cdff 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ You can also browse the documentation at https://constructs.dev/packages/cdk-mon | AWS Billing (`.monitorBilling()`) | AWS account cost | | [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 | | +| AWS CloudWatch Synthetics Canary (`.monitorSyntheticsCanary()`) | Latency, error count/rate | Error count/rate, latency | | | AWS CodeBuild (`.monitorCodeBuildProject()`) | Build counts (total, successful, failed), failed rate, duration | Failed build count/rate, duration | | | AWS DynamoDB (`.monitorDynamoTable()`) | Read and write capacity provisioned / used | Consumed capacity, throttling, latency, errors | | | AWS DynamoDB Global Secondary Index (`.monitorDynamoTableGlobalSecondaryIndex()`) | Read and write capacity, indexing progress, throttled events | | | diff --git a/lib/facade/MonitoringAspect.ts b/lib/facade/MonitoringAspect.ts index 176822b3..9411da48 100644 --- a/lib/facade/MonitoringAspect.ts +++ b/lib/facade/MonitoringAspect.ts @@ -21,6 +21,7 @@ import * as secretsmanager from "monocdk/aws-secretsmanager"; import * as sns from "monocdk/aws-sns"; import * as sqs from "monocdk/aws-sqs"; import * as stepfunctions from "monocdk/aws-stepfunctions"; +import * as synthetics from "monocdk/aws-synthetics"; import { ElastiCacheClusterType } from "../monitoring"; import { MonitoringAspectProps, MonitoringAspectType } from "./aspect-types"; @@ -61,7 +62,8 @@ export class MonitoringAspect implements IAspect { this.monitorSecretsManager(node); this.monitorSns(node); this.monitorSqs(node); - this.monitorStepFuntions(node); + this.monitorStepFunctions(node); + this.monitorSyntheticsCanaries(node); if (!this.addedNodeIndependentMonitoringToScope) { this.addedNodeIndependentMonitoringToScope = true; @@ -340,7 +342,7 @@ export class MonitoringAspect implements IAspect { } } - private monitorStepFuntions(node: IConstruct) { + private monitorStepFunctions(node: IConstruct) { const [isEnabled, props] = this.getMonitoringDetails( this.props.stepFunctions ); @@ -351,6 +353,18 @@ export class MonitoringAspect implements IAspect { }); } } + + private monitorSyntheticsCanaries(node: IConstruct) { + const [isEnabled, props] = this.getMonitoringDetails( + this.props.syntheticsCanaries + ); + if (isEnabled && node instanceof synthetics.Canary) { + this.monitoringFacade.monitorSyntheticsCanary({ + canary: node, + ...props, + }); + } + } } export * from "./aspect-types"; diff --git a/lib/facade/MonitoringFacade.ts b/lib/facade/MonitoringFacade.ts index 5e15caa2..0145889a 100644 --- a/lib/facade/MonitoringFacade.ts +++ b/lib/facade/MonitoringFacade.ts @@ -101,6 +101,8 @@ import { StepFunctionServiceIntegrationMonitoringProps, getQueueProcessingEc2ServiceMonitoring, getQueueProcessingFargateServiceMonitoring, + SyntheticsCanaryMonitoringProps, + SyntheticsCanaryMonitoring, } from "../monitoring"; import { MonitoringAspect, MonitoringAspectProps } from "./MonitoringAspect"; @@ -605,6 +607,12 @@ export class MonitoringFacade extends MonitoringScope { return this; } + monitorSyntheticsCanary(props: SyntheticsCanaryMonitoringProps) { + const segment = new SyntheticsCanaryMonitoring(this, props); + this.addSegment(segment, props); + return this; + } + monitorBilling(props?: BillingMonitoringProps) { const segment = new BillingMonitoring(this, props ?? {}); this.addSegment(segment, props); diff --git a/lib/facade/aspect-types.ts b/lib/facade/aspect-types.ts index fe6e2415..8b02b7d5 100644 --- a/lib/facade/aspect-types.ts +++ b/lib/facade/aspect-types.ts @@ -23,6 +23,7 @@ import { SnsTopicMonitoringOptions, SqsQueueMonitoringOptions, StepFunctionMonitoringOptions, + SyntheticsCanaryMonitoringOptions, } from "../monitoring"; export interface MonitoringAspectType { @@ -64,4 +65,5 @@ export interface MonitoringAspectProps { readonly sns?: MonitoringAspectType; readonly sqs?: MonitoringAspectType; readonly stepFunctions?: MonitoringAspectType; + readonly syntheticsCanaries?: MonitoringAspectType; } diff --git a/lib/monitoring/aws-synthetics/SyntheticsCanaryMetricFactory.ts b/lib/monitoring/aws-synthetics/SyntheticsCanaryMetricFactory.ts new file mode 100644 index 00000000..17ef1d1f --- /dev/null +++ b/lib/monitoring/aws-synthetics/SyntheticsCanaryMetricFactory.ts @@ -0,0 +1,101 @@ +import { DimensionHash } from "monocdk/aws-cloudwatch"; +import { Canary } from "monocdk/aws-synthetics"; +import { + MetricFactory, + MetricStatistic, + RateComputationMethod, +} from "../../common/index"; + +const MetricNamespace = "CloudWatchSynthetics"; + +export interface SyntheticsCanaryMetricFactoryProps { + /** + * CloudWatch Canary to monitor + */ + readonly canary: Canary; + /** + * Method used to calculate relative rates + * @default average + */ + readonly rateComputationMethod?: RateComputationMethod; +} + +export class SyntheticsCanaryMetricFactory { + protected readonly canary: Canary; + protected readonly metricFactory: MetricFactory; + protected readonly rateComputationMethod: RateComputationMethod; + protected readonly dimensions: DimensionHash; + + constructor( + metricFactory: MetricFactory, + props: SyntheticsCanaryMetricFactoryProps + ) { + this.canary = props.canary; + this.metricFactory = metricFactory; + this.rateComputationMethod = + props.rateComputationMethod ?? RateComputationMethod.AVERAGE; + this.dimensions = { + CanaryName: props.canary.canaryName, + }; + } + + metricLatencyAverageInMillis() { + return this.metricFactory.adaptMetric( + this.canary.metricDuration({ + label: "Average", + statistic: MetricStatistic.AVERAGE, + }) + ); + } + + metricSuccessInPercent() { + return this.metricFactory.adaptMetric( + this.canary.metricSuccessPercent({ + label: "Success Rate", + statistic: MetricStatistic.AVERAGE, + }) + ); + } + + metric4xxErrorCount() { + return this.metricFactory.createMetric( + "4xx", + MetricStatistic.SUM, + "4xx", + this.dimensions, + undefined, + MetricNamespace + ); + } + + metric4xxErrorRate() { + const metric = this.metric4xxErrorCount(); + return this.metricFactory.toRate( + metric, + this.rateComputationMethod, + false, + "errors" + ); + } + + metric5xxFaultCount() { + return this.metricFactory.createMetric( + "5xx", + MetricStatistic.SUM, + "5xx", + this.dimensions, + undefined, + MetricNamespace + ); + } + + metric5xxFaultRate() { + const metric = this.metric5xxFaultCount(); + return this.metricFactory.toRate( + metric, + this.rateComputationMethod, + false, + "faults" + ); + } +} diff --git a/lib/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.ts b/lib/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.ts new file mode 100644 index 00000000..42745e83 --- /dev/null +++ b/lib/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.ts @@ -0,0 +1,211 @@ +import { + GraphWidget, + HorizontalAnnotation, + IWidget, +} from "monocdk/aws-cloudwatch"; +import { + BaseMonitoringProps, + CountAxisFromZero, + DefaultGraphWidgetHeight, + DefaultSummaryWidgetHeight, + ErrorAlarmFactory, + ErrorCountThreshold, + ErrorRateThreshold, + ErrorType, + HalfWidth, + LatencyAlarmFactory, + LatencyThreshold, + LatencyType, + MetricWithAlarmSupport, + Monitoring, + MonitoringScope, + RateAxisFromZero, + ThirdWidth, + TimeAxisMillisFromZero, +} from "../../common/index"; +import { + MonitoringHeaderWidget, + MonitoringNamingStrategy, +} from "../../dashboard/index"; +import { + SyntheticsCanaryMetricFactory, + SyntheticsCanaryMetricFactoryProps, +} from "./SyntheticsCanaryMetricFactory"; + +export interface SyntheticsCanaryMonitoringOptions extends BaseMonitoringProps { + readonly addAverageLatencyAlarm?: Record; + readonly add4xxErrorCountAlarm?: Record; + readonly add4xxErrorRateAlarm?: Record; + readonly add5xxFaultCountAlarm?: Record; + readonly add5xxFaultRateAlarm?: Record; +} + +export interface SyntheticsCanaryMonitoringProps + extends SyntheticsCanaryMetricFactoryProps, + SyntheticsCanaryMonitoringOptions {} + +/** + * Monitoring for CloudWatch Synthetics Canaries. + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html + */ +export class SyntheticsCanaryMonitoring extends Monitoring { + protected readonly humanReadableName: string; + + protected readonly latencyAlarmFactory: LatencyAlarmFactory; + protected readonly errorAlarmFactory: ErrorAlarmFactory; + protected readonly latencyAnnotations: HorizontalAnnotation[]; + protected readonly errorCountAnnotations: HorizontalAnnotation[]; + protected readonly errorRateAnnotations: HorizontalAnnotation[]; + + protected readonly averageLatencyMetric: MetricWithAlarmSupport; + protected readonly errorCountMetric: MetricWithAlarmSupport; + protected readonly errorRateMetric: MetricWithAlarmSupport; + protected readonly faultCountMetric: MetricWithAlarmSupport; + protected readonly faultRateMetric: MetricWithAlarmSupport; + + constructor(scope: MonitoringScope, props: SyntheticsCanaryMonitoringProps) { + super(scope, props); + + const namingStrategy = new MonitoringNamingStrategy({ + ...props, + fallbackConstructName: props.canary.canaryName, + namedConstruct: props.canary, + }); + this.humanReadableName = namingStrategy.resolveHumanReadableName(); + + const alarmFactory = this.createAlarmFactory( + namingStrategy.resolveAlarmFriendlyName() + ); + this.latencyAlarmFactory = new LatencyAlarmFactory(alarmFactory); + this.errorAlarmFactory = new ErrorAlarmFactory(alarmFactory); + this.latencyAnnotations = []; + this.errorCountAnnotations = []; + this.errorRateAnnotations = []; + + const metricFactory = new SyntheticsCanaryMetricFactory( + scope.createMetricFactory(), + props + ); + this.averageLatencyMetric = metricFactory.metricLatencyAverageInMillis(); + this.errorCountMetric = metricFactory.metric4xxErrorCount(); + this.errorRateMetric = metricFactory.metric4xxErrorRate(); + this.faultCountMetric = metricFactory.metric5xxFaultCount(); + this.faultRateMetric = metricFactory.metric5xxFaultRate(); + + for (const disambiguator in props.addAverageLatencyAlarm) { + const alarmProps = props.addAverageLatencyAlarm[disambiguator]; + const createdAlarm = this.latencyAlarmFactory.addLatencyAlarm( + this.averageLatencyMetric, + LatencyType.AVERAGE, + alarmProps, + disambiguator + ); + this.latencyAnnotations.push(createdAlarm.annotation); + this.addAlarm(createdAlarm); + } + for (const disambiguator in props.add4xxErrorCountAlarm) { + const alarmProps = props.add4xxErrorCountAlarm[disambiguator]; + const createdAlarm = this.errorAlarmFactory.addErrorCountAlarm( + this.errorCountMetric, + ErrorType.ERROR, + alarmProps, + disambiguator + ); + this.errorCountAnnotations.push(createdAlarm.annotation); + this.addAlarm(createdAlarm); + } + for (const disambiguator in props.add4xxErrorRateAlarm) { + const alarmProps = props.add4xxErrorRateAlarm[disambiguator]; + const createdAlarm = this.errorAlarmFactory.addErrorRateAlarm( + this.errorRateMetric, + ErrorType.ERROR, + alarmProps, + disambiguator + ); + this.errorRateAnnotations.push(createdAlarm.annotation); + this.addAlarm(createdAlarm); + } + for (const disambiguator in props.add5xxFaultCountAlarm) { + const alarmProps = props.add5xxFaultCountAlarm[disambiguator]; + const createdAlarm = this.errorAlarmFactory.addErrorCountAlarm( + this.faultCountMetric, + ErrorType.FAULT, + alarmProps, + disambiguator + ); + this.errorCountAnnotations.push(createdAlarm.annotation); + this.addAlarm(createdAlarm); + } + for (const disambiguator in props.add5xxFaultRateAlarm) { + const alarmProps = props.add5xxFaultRateAlarm[disambiguator]; + const createdAlarm = this.errorAlarmFactory.addErrorRateAlarm( + this.faultRateMetric, + ErrorType.FAULT, + alarmProps, + disambiguator + ); + this.errorRateAnnotations.push(createdAlarm.annotation); + this.addAlarm(createdAlarm); + } + + props.useCreatedAlarms?.consume(this.createdAlarms()); + } + + summaryWidgets(): IWidget[] { + return [ + this.createTitleWidget(), + this.createErrorCountWidget(HalfWidth, DefaultSummaryWidgetHeight), + this.createErrorRateWidget(HalfWidth, DefaultSummaryWidgetHeight), + ]; + } + + widgets(): IWidget[] { + return [ + this.createTitleWidget(), + this.createLatencyWidget(ThirdWidth, DefaultGraphWidgetHeight), + this.createErrorCountWidget(ThirdWidth, DefaultGraphWidgetHeight), + this.createErrorRateWidget(ThirdWidth, DefaultGraphWidgetHeight), + ]; + } + + protected createTitleWidget() { + return new MonitoringHeaderWidget({ + family: "Synthetics Canary", + title: this.humanReadableName, + }); + } + + protected createLatencyWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Latency", + left: [this.averageLatencyMetric], + leftYAxis: TimeAxisMillisFromZero, + leftAnnotations: this.latencyAnnotations, + }); + } + + protected createErrorCountWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Errors", + left: [this.errorCountMetric, this.faultCountMetric], + leftAnnotations: this.errorCountAnnotations, + leftYAxis: CountAxisFromZero, + }); + } + + protected createErrorRateWidget(width: number, height: number) { + return new GraphWidget({ + width, + height, + title: "Errors (rate)", + left: [this.errorRateMetric, this.faultRateMetric], + leftYAxis: RateAxisFromZero, + leftAnnotations: this.errorRateAnnotations, + }); + } +} diff --git a/lib/monitoring/aws-synthetics/index.ts b/lib/monitoring/aws-synthetics/index.ts new file mode 100644 index 00000000..0c950c24 --- /dev/null +++ b/lib/monitoring/aws-synthetics/index.ts @@ -0,0 +1,2 @@ +export * from "./SyntheticsCanaryMetricFactory"; +export * from "./SyntheticsCanaryMonitoring"; diff --git a/lib/monitoring/index.ts b/lib/monitoring/index.ts index 3665be2b..e68414f2 100644 --- a/lib/monitoring/index.ts +++ b/lib/monitoring/index.ts @@ -23,4 +23,5 @@ export * from "./aws-secretsmanager"; export * from "./aws-sns"; export * from "./aws-sqs"; export * from "./aws-step-functions"; +export * from "./aws-synthetics"; export * from "./custom"; diff --git a/test/facade/MonitoringAspect.test.ts b/test/facade/MonitoringAspect.test.ts index 22d0e75c..c5ebfdc6 100644 --- a/test/facade/MonitoringAspect.test.ts +++ b/test/facade/MonitoringAspect.test.ts @@ -1,4 +1,4 @@ -import { App, SecretValue, Stack } from "monocdk"; +import { App, Duration, SecretValue, Stack } from "monocdk"; import { Template } from "monocdk/assertions"; import * as apigw from "monocdk/aws-apigateway"; import * as apigwv2 from "monocdk/aws-apigatewayv2"; @@ -25,6 +25,7 @@ import * as secretsmanager from "monocdk/aws-secretsmanager"; import * as sns from "monocdk/aws-sns"; import * as sqs from "monocdk/aws-sqs"; import * as stepfunctions from "monocdk/aws-stepfunctions"; +import * as synthetics from "monocdk/aws-synthetics"; import { DefaultDashboardFactory, @@ -513,4 +514,25 @@ describe("MonitoringAspect", () => { // THEN expect(Template.fromStack(stack)).toMatchSnapshot(); }); + + test("Canaries", () => { + // GIVEN + const stack = new Stack(); + const facade = createDummyMonitoringFacade(stack); + + new synthetics.Canary(stack, "Canary", { + schedule: synthetics.Schedule.rate(Duration.minutes(5)), + test: synthetics.Test.custom({ + code: synthetics.Code.fromInline("/* nothing */"), + handler: "index.handler", + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + }); + + // WHEN + facade.monitorScope(stack, defaultAspectProps); + + // THEN + expect(Template.fromStack(stack)).toMatchSnapshot(); + }); }); diff --git a/test/facade/__snapshots__/MonitoringAspect.test.ts.snap b/test/facade/__snapshots__/MonitoringAspect.test.ts.snap index f1f333c9..dd3a0fba 100644 --- a/test/facade/__snapshots__/MonitoringAspect.test.ts.snap +++ b/test/facade/__snapshots__/MonitoringAspect.test.ts.snap @@ -1266,6 +1266,284 @@ Object { } `; +exports[`MonitoringAspect Canaries 1`] = ` +Object { + "Resources": Object { + "Canary11957FE2": Object { + "Properties": Object { + "ArtifactS3Location": Object { + "Fn::Join": Array [ + "", + Array [ + "s3://", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + ], + ], + }, + "Code": Object { + "Handler": "index.handler", + "Script": "/* nothing */", + }, + "ExecutionRoleArn": Object { + "Fn::GetAtt": Array [ + "CanaryServiceRoleD132250E", + "Arn", + ], + }, + "Name": "canary", + "RuntimeVersion": "syn-nodejs-2.0", + "Schedule": Object { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)", + }, + "StartCanaryAfterCreation": true, + }, + "Type": "AWS::Synthetics::Canary", + }, + "CanaryArtifactsBucket4A60D32B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CanaryServiceRoleD132250E": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "s3:PutObject", + "s3:GetBucketLocation", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CanaryArtifactsBucket4A60D32B", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + Object { + "Action": "cloudwatch:PutMetricData", + "Condition": Object { + "StringEquals": Object { + "cloudwatch:namespace": "CloudWatchSynthetics", + }, + }, + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:::*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "canaryPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "DashboardFactoryAlarmDashboard6286FAD3": Object { + "Properties": Object { + "DashboardBody": "{\\"start\\":\\"-PT8H\\",\\"widgets\\":[]}", + "DashboardName": "DummyDashboard-Alarms", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "DashboardFactoryDashboard3E20AD6E": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-PT8H\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Synthetics Canary **Canary**\\"}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Latency\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"CloudWatchSynthetics\\",\\"Duration\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"Average\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"ms\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":8,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"CloudWatchSynthetics\\",\\"4xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"4xx\\",\\"stat\\":\\"Sum\\"}],[\\"CloudWatchSynthetics\\",\\"5xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"5xx\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":8,\\"height\\":5,\\"x\\":16,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"CloudWatchSynthetics\\",\\"4xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"4xx (avg)\\"}],[\\"CloudWatchSynthetics\\",\\"5xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"5xx (avg)\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":6,\\"properties\\":{\\"markdown\\":\\"### S3 Bucket **[ArtifactsBucket](https://s3.console.aws.amazon.com/s3/buckets/", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "?region=", + Object { + "Ref": "AWS::Region", + }, + "&tab=metrics)**\\"}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":5,\\"x\\":0,\\"y\\":7,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Bucket Size\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/S3\\",\\"BucketSizeBytes\\",\\"BucketName\\",\\"", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "\\",\\"StorageType\\",\\"StandardStorage\\",{\\"label\\":\\"BucketSizeBytes\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"bytes\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":5,\\"x\\":12,\\"y\\":7,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Object Count\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/S3\\",\\"NumberOfObjects\\",\\"BucketName\\",\\"", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "\\",\\"StorageType\\",\\"AllStorageTypes\\",{\\"label\\":\\"NumberOfObjects\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "DummyDashboard", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + "DashboardFactorySummaryDashboard5F4BC8C5": Object { + "Properties": Object { + "DashboardBody": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"start\\":\\"-P14D\\",\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### Synthetics Canary **Canary**\\"}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":6,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"CloudWatchSynthetics\\",\\"4xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"4xx\\",\\"stat\\":\\"Sum\\"}],[\\"CloudWatchSynthetics\\",\\"5xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"5xx\\",\\"stat\\":\\"Sum\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":6,\\"x\\":12,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Errors (rate)\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"CloudWatchSynthetics\\",\\"4xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"4xx (avg)\\"}],[\\"CloudWatchSynthetics\\",\\"5xx\\",\\"CanaryName\\",\\"", + Object { + "Ref": "Canary11957FE2", + }, + "\\",{\\"label\\":\\"5xx (avg)\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Rate\\",\\"showUnits\\":false}}}},{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":7,\\"properties\\":{\\"markdown\\":\\"### S3 Bucket **[ArtifactsBucket](https://s3.console.aws.amazon.com/s3/buckets/", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "?region=", + Object { + "Ref": "AWS::Region", + }, + "&tab=metrics)**\\"}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":6,\\"x\\":0,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Bucket Size\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/S3\\",\\"BucketSizeBytes\\",\\"BucketName\\",\\"", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "\\",\\"StorageType\\",\\"StandardStorage\\",{\\"label\\":\\"BucketSizeBytes\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"bytes\\",\\"showUnits\\":false}}}},{\\"type\\":\\"metric\\",\\"width\\":12,\\"height\\":6,\\"x\\":12,\\"y\\":8,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"Object Count\\",\\"region\\":\\"", + Object { + "Ref": "AWS::Region", + }, + "\\",\\"metrics\\":[[\\"AWS/S3\\",\\"NumberOfObjects\\",\\"BucketName\\",\\"", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + "\\",\\"StorageType\\",\\"AllStorageTypes\\",{\\"label\\":\\"NumberOfObjects\\"}]],\\"yAxis\\":{\\"left\\":{\\"min\\":0,\\"label\\":\\"Count\\",\\"showUnits\\":false}}}}]}", + ], + ], + }, + "DashboardName": "DummyDashboard-Summary", + }, + "Type": "AWS::CloudWatch::Dashboard", + }, + }, +} +`; + exports[`MonitoringAspect CloudFront 1`] = ` Object { "Resources": Object { diff --git a/test/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.test.ts b/test/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.test.ts new file mode 100644 index 00000000..1a239a53 --- /dev/null +++ b/test/monitoring/aws-synthetics/SyntheticsCanaryMonitoring.test.ts @@ -0,0 +1,78 @@ +import { Duration, Stack } from "monocdk"; +import { Template } from "monocdk/assertions"; +import { Canary, Code, Runtime, Schedule, Test } from "monocdk/aws-synthetics"; + +import { AlarmWithAnnotation } from "../../../lib"; +import { SyntheticsCanaryMonitoring } from "../../../lib/monitoring/aws-synthetics"; +import { TestMonitoringScope } from "../TestMonitoringScope"; + +test("snapshot test: no alarms", () => { + const stack = new Stack(); + const canary = new Canary(stack, "Canary", { + schedule: Schedule.rate(Duration.minutes(5)), + test: Test.custom({ + code: Code.fromInline("/* nothing */"), + handler: "index.handler", // must end with '.handler' + }), + runtime: Runtime.SYNTHETICS_NODEJS_2_0, + }); + + const scope = new TestMonitoringScope(stack, "Scope"); + + new SyntheticsCanaryMonitoring(scope, { canary }); + + expect(Template.fromStack(stack)).toMatchSnapshot(); +}); + +test("snapshot test: all alarms", () => { + const stack = new Stack(); + const canary = new Canary(stack, "Canary", { + schedule: Schedule.rate(Duration.minutes(5)), + test: Test.custom({ + code: Code.fromInline("/* nothing */"), + handler: "index.handler", // must end with '.handler' + }), + runtime: Runtime.SYNTHETICS_NODEJS_2_0, + }); + + const scope = new TestMonitoringScope(stack, "Scope"); + + let numAlarmsCreated = 0; + + new SyntheticsCanaryMonitoring(scope, { + canary, + addAverageLatencyAlarm: { + Warning: { + maxLatency: Duration.seconds(10), + }, + }, + add4xxErrorCountAlarm: { + Warning: { + maxErrorCount: 1, + }, + }, + add5xxFaultCountAlarm: { + Warning: { + maxErrorCount: 2, + }, + }, + add4xxErrorRateAlarm: { + Warning: { + maxErrorRate: 0.5, + }, + }, + add5xxFaultRateAlarm: { + Warning: { + maxErrorRate: 0.8, + }, + }, + useCreatedAlarms: { + consume(alarms: AlarmWithAnnotation[]) { + numAlarmsCreated = alarms.length; + }, + }, + }); + + expect(numAlarmsCreated).toStrictEqual(5); + expect(Template.fromStack(stack)).toMatchSnapshot(); +}); diff --git a/test/monitoring/aws-synthetics/__snapshots__/SyntheticsCanaryMonitoring.test.ts.snap b/test/monitoring/aws-synthetics/__snapshots__/SyntheticsCanaryMonitoring.test.ts.snap new file mode 100644 index 00000000..e9f8aa45 --- /dev/null +++ b/test/monitoring/aws-synthetics/__snapshots__/SyntheticsCanaryMonitoring.test.ts.snap @@ -0,0 +1,459 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test: all alarms 1`] = ` +Object { + "Resources": Object { + "Canary11957FE2": Object { + "Properties": Object { + "ArtifactS3Location": Object { + "Fn::Join": Array [ + "", + Array [ + "s3://", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + ], + ], + }, + "Code": Object { + "Handler": "index.handler", + "Script": "/* nothing */", + }, + "ExecutionRoleArn": Object { + "Fn::GetAtt": Array [ + "CanaryServiceRoleD132250E", + "Arn", + ], + }, + "Name": "canary", + "RuntimeVersion": "syn-nodejs-2.0", + "Schedule": Object { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)", + }, + "StartCanaryAfterCreation": true, + }, + "Type": "AWS::Synthetics::Canary", + }, + "CanaryArtifactsBucket4A60D32B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CanaryServiceRoleD132250E": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "s3:PutObject", + "s3:GetBucketLocation", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CanaryArtifactsBucket4A60D32B", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + Object { + "Action": "cloudwatch:PutMetricData", + "Condition": Object { + "StringEquals": Object { + "cloudwatch:namespace": "CloudWatchSynthetics", + }, + }, + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:::*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "canaryPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "ScopeTestCanaryErrorCountWarningE11B5E8D": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmDescription": "Error count is too high.", + "AlarmName": "Test-Canary-Error-Count-Warning", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "4xx", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "CanaryName", + "Value": Object { + "Ref": "Canary11957FE2", + }, + }, + ], + "MetricName": "4xx", + "Namespace": "CloudWatchSynthetics", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": true, + }, + ], + "Threshold": 1, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "ScopeTestCanaryErrorRateWarning879600BF": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmDescription": "Error rate is too high.", + "AlarmName": "Test-Canary-Error-Rate-Warning", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "4xx (avg)", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "CanaryName", + "Value": Object { + "Ref": "Canary11957FE2", + }, + }, + ], + "MetricName": "4xx", + "Namespace": "CloudWatchSynthetics", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 0.5, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "ScopeTestCanaryFaultCountWarningBE79A4B2": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmDescription": "Fault count is too high.", + "AlarmName": "Test-Canary-Fault-Count-Warning", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "5xx", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "CanaryName", + "Value": Object { + "Ref": "Canary11957FE2", + }, + }, + ], + "MetricName": "5xx", + "Namespace": "CloudWatchSynthetics", + }, + "Period": 300, + "Stat": "Sum", + }, + "ReturnData": true, + }, + ], + "Threshold": 2, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "ScopeTestCanaryFaultRateWarning65C6A70B": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmDescription": "Fault rate is too high.", + "AlarmName": "Test-Canary-Fault-Rate-Warning", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "5xx (avg)", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "CanaryName", + "Value": Object { + "Ref": "Canary11957FE2", + }, + }, + ], + "MetricName": "5xx", + "Namespace": "CloudWatchSynthetics", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 0.8, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "ScopeTestCanaryLatencyAverageWarning318006A9": Object { + "Properties": Object { + "ActionsEnabled": true, + "AlarmDescription": "Average latency is too high.", + "AlarmName": "Test-Canary-Latency-Average-Warning", + "ComparisonOperator": "GreaterThanThreshold", + "DatapointsToAlarm": 3, + "EvaluationPeriods": 3, + "Metrics": Array [ + Object { + "Id": "m1", + "Label": "Average", + "MetricStat": Object { + "Metric": Object { + "Dimensions": Array [ + Object { + "Name": "CanaryName", + "Value": Object { + "Ref": "Canary11957FE2", + }, + }, + ], + "MetricName": "Duration", + "Namespace": "CloudWatchSynthetics", + }, + "Period": 300, + "Stat": "Average", + }, + "ReturnData": true, + }, + ], + "Threshold": 10000, + "TreatMissingData": "notBreaching", + }, + "Type": "AWS::CloudWatch::Alarm", + }, + }, +} +`; + +exports[`snapshot test: no alarms 1`] = ` +Object { + "Resources": Object { + "Canary11957FE2": Object { + "Properties": Object { + "ArtifactS3Location": Object { + "Fn::Join": Array [ + "", + Array [ + "s3://", + Object { + "Ref": "CanaryArtifactsBucket4A60D32B", + }, + ], + ], + }, + "Code": Object { + "Handler": "index.handler", + "Script": "/* nothing */", + }, + "ExecutionRoleArn": Object { + "Fn::GetAtt": Array [ + "CanaryServiceRoleD132250E", + "Arn", + ], + }, + "Name": "canary", + "RuntimeVersion": "syn-nodejs-2.0", + "Schedule": Object { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)", + }, + "StartCanaryAfterCreation": true, + }, + "Type": "AWS::Synthetics::Canary", + }, + "CanaryArtifactsBucket4A60D32B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CanaryServiceRoleD132250E": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "s3:PutObject", + "s3:GetBucketLocation", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CanaryArtifactsBucket4A60D32B", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + Object { + "Action": "cloudwatch:PutMetricData", + "Condition": Object { + "StringEquals": Object { + "cloudwatch:namespace": "CloudWatchSynthetics", + }, + }, + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:::*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "canaryPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, +} +`;