diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index d2e37108797cd..cfb4680987d2c 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -2184,7 +2184,7 @@ integTest( integTest( 'hotswap deployment for ecs service detects failed deployment and errors', - withDefaultFixture(async (fixture) => { + withExtendedTimeoutFixture(async (fixture) => { // GIVEN await fixture.cdkDeploy('ecs-hotswap'); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets.json new file mode 100644 index 0000000000000..7e6afd7d1b6b3 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/LogGroupMetricsDefaultTestDeployAssertF61C3BCA.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.assets.json new file mode 100644 index 0000000000000..008e4c48fcd0f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "71b53c3965d74bf965044819161cded75e176bd07805af4fceff5dcda7eab49e": { + "source": { + "path": "aws-cdk-log-group-metrics.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "71b53c3965d74bf965044819161cded75e176bd07805af4fceff5dcda7eab49e.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.template.json new file mode 100644 index 0000000000000..1a2cf93f3a73d --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/aws-cdk-log-group-metrics.template.json @@ -0,0 +1,71 @@ +{ + "Resources": { + "MyLogGroup5C0DAD85": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "my-log-group", + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "IncomingBytesPerInstanceAlarmFA8EEFDB": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "IncomingBytes", + "Namespace": "AWS/Logs", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "IncomingEventsPerInstanceAlarmA9670D55": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "IncomingLogs", + "Namespace": "AWS/Logs", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c6e612584e352 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/integ.json new file mode 100644 index 0000000000000..af6adfec747c4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "38.0.1", + "testCases": { + "LogGroupMetrics/DefaultTest": { + "stacks": [ + "aws-cdk-log-group-metrics" + ], + "assertionStack": "LogGroupMetrics/DefaultTest/DeployAssert", + "assertionStackName": "LogGroupMetricsDefaultTestDeployAssertF61C3BCA" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/manifest.json new file mode 100644 index 0000000000000..ebdb79361913c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/manifest.json @@ -0,0 +1,127 @@ +{ + "version": "38.0.1", + "artifacts": { + "aws-cdk-log-group-metrics.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-log-group-metrics.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-log-group-metrics": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-log-group-metrics.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/71b53c3965d74bf965044819161cded75e176bd07805af4fceff5dcda7eab49e.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-log-group-metrics.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-log-group-metrics.assets" + ], + "metadata": { + "/aws-cdk-log-group-metrics/MyLogGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyLogGroup5C0DAD85" + } + ], + "/aws-cdk-log-group-metrics/IncomingBytesPerInstanceAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "IncomingBytesPerInstanceAlarmFA8EEFDB" + } + ], + "/aws-cdk-log-group-metrics/IncomingEventsPerInstanceAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "IncomingEventsPerInstanceAlarmA9670D55" + } + ], + "/aws-cdk-log-group-metrics/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-log-group-metrics/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-log-group-metrics" + }, + "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "LogGroupMetricsDefaultTestDeployAssertF61C3BCA": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "LogGroupMetricsDefaultTestDeployAssertF61C3BCA.assets" + ], + "metadata": { + "/LogGroupMetrics/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/LogGroupMetrics/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "LogGroupMetrics/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/tree.json new file mode 100644 index 0000000000000..c68d2c1c9b17a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.js.snapshot/tree.json @@ -0,0 +1,186 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-log-group-metrics": { + "id": "aws-cdk-log-group-metrics", + "path": "aws-cdk-log-group-metrics", + "children": { + "MyLogGroup": { + "id": "MyLogGroup", + "path": "aws-cdk-log-group-metrics/MyLogGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-log-group-metrics/MyLogGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Logs::LogGroup", + "aws:cdk:cloudformation:props": { + "logGroupName": "my-log-group", + "retentionInDays": 731 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_logs.CfnLogGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_logs.LogGroup", + "version": "0.0.0" + } + }, + "IncomingBytesPerInstanceAlarm": { + "id": "IncomingBytesPerInstanceAlarm", + "path": "aws-cdk-log-group-metrics/IncomingBytesPerInstanceAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-log-group-metrics/IncomingBytesPerInstanceAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "metricName": "IncomingBytes", + "namespace": "AWS/Logs", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "IncomingEventsPerInstanceAlarm": { + "id": "IncomingEventsPerInstanceAlarm", + "path": "aws-cdk-log-group-metrics/IncomingEventsPerInstanceAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-log-group-metrics/IncomingEventsPerInstanceAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "metricName": "IncomingLogs", + "namespace": "AWS/Logs", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-log-group-metrics/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-log-group-metrics/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "LogGroupMetrics": { + "id": "LogGroupMetrics", + "path": "LogGroupMetrics", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "LogGroupMetrics/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "LogGroupMetrics/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "LogGroupMetrics/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "LogGroupMetrics/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "LogGroupMetrics/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.ts new file mode 100644 index 0000000000000..ea74dec2462f5 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.log-group-metrics.ts @@ -0,0 +1,25 @@ +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { App, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { LogGroup } from 'aws-cdk-lib/aws-logs'; + +const app = new App(); +const stack = new Stack(app, 'aws-cdk-log-group-metrics'); + +const logGroup = new LogGroup(stack, 'MyLogGroup', { + logGroupName: 'my-log-group', + removalPolicy: RemovalPolicy.DESTROY, +}); + +logGroup.metricIncomingBytes().createAlarm(stack, 'IncomingBytesPerInstanceAlarm', { + threshold: 1, + evaluationPeriods: 1, +}); + +logGroup.metricIncomingLogEvents().createAlarm(stack, 'IncomingEventsPerInstanceAlarm', { + threshold: 1, + evaluationPeriods: 1, +}); + +new IntegTest(app, 'LogGroupMetrics', { + testCases: [stack], +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-events-targets/test/logs/log-group.test.ts b/packages/aws-cdk-lib/aws-events-targets/test/logs/log-group.test.ts index f2670af087148..0f831b032168f 100644 --- a/packages/aws-cdk-lib/aws-events-targets/test/logs/log-group.test.ts +++ b/packages/aws-cdk-lib/aws-events-targets/test/logs/log-group.test.ts @@ -398,3 +398,83 @@ testDeprecated('specifying retry policy with 0 retryAttempts', () => { ], }); }); + +test('metricIncomingLogEvents', () => { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = new logs.LogGroup(stack, 'MyLogGroup', { + logGroupName: '/aws/events/MyLogGroup', + }); + + expect(stack.resolve(logGroup.metricIncomingLogEvents())).toEqual({ + period: { + amount: 5, + unit: { label: 'minutes', inMillis: 60000, isoLabel: 'M' }, + }, + namespace: 'AWS/Logs', + metricName: 'IncomingLogs', + statistic: 'Sum', + }); +}); + +test('metricIncomingLogEvents with MetricOptions props', () => { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = new logs.LogGroup(stack, 'MyLogGroup', { + logGroupName: '/aws/events/MyLogGroup', + }); + + expect(stack.resolve(logGroup.metricIncomingLogEvents({ + period: cdk.Duration.hours(10), + label: 'MyMetric', + }))).toEqual({ + period: { + amount: 10, + unit: { label: 'hours', inMillis: 3600000, isoLabel: 'H' }, + }, + namespace: 'AWS/Logs', + metricName: 'IncomingLogs', + statistic: 'Sum', + label: 'MyMetric', + }); +}); + +test('metricIncomingBytes', () => { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = new logs.LogGroup(stack, 'MyLogGroup', { + logGroupName: '/aws/events/MyLogGroup', + }); + + expect(stack.resolve(logGroup.metricIncomingBytes())).toEqual({ + period: { + amount: 5, + unit: { label: 'minutes', inMillis: 60000, isoLabel: 'M' }, + }, + namespace: 'AWS/Logs', + metricName: 'IncomingBytes', + statistic: 'Sum', + }); +}); + +test('metricIncomingBytes with MetricOptions props', () => { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = new logs.LogGroup(stack, 'MyLogGroup', { + logGroupName: '/aws/events/MyLogGroup', + }); + + expect(stack.resolve(logGroup.metricIncomingBytes({ + period: cdk.Duration.minutes(15), + statistic: 'Sum', + }))).toEqual({ + period: { + amount: 15, + unit: { label: 'minutes', inMillis: 60000, isoLabel: 'M' }, + }, + namespace: 'AWS/Logs', + metricName: 'IncomingBytes', + statistic: 'Sum', + }); +}); + diff --git a/packages/aws-cdk-lib/aws-logs/README.md b/packages/aws-cdk-lib/aws-logs/README.md index 4ee04e1de7ed3..52dd9aaf84b1f 100644 --- a/packages/aws-cdk-lib/aws-logs/README.md +++ b/packages/aws-cdk-lib/aws-logs/README.md @@ -208,6 +208,29 @@ new cloudwatch.Alarm(this, 'alarm from metric filter', { }); ``` +### Metrics for IncomingLogs and IncomingBytes +Metric methods have been defined for IncomingLogs and IncomingBytes within LogGroups. These metrics allow for the creation of alarms on log ingestion, ensuring that the log ingestion process is functioning correctly. + +To define an alarm based on these metrics, you can use the following template: +```ts +const logGroup = new logs.LogGroup(this, 'MyLogGroup'); +const incomingEventsMetric = logGroup.metricIncomingLogEvents(); +new cloudwatch.Alarm(this, 'HighLogVolumeAlarm', { + metric: incomingEventsMetric, + threshold: 1000, + evaluationPeriods: 1, +}); +``` +```ts +const logGroup = new logs.LogGroup(this, 'MyLogGroup'); +const incomingBytesMetric = logGroup.metricIncomingBytes(); +new cloudwatch.Alarm(this, 'HighDataVolumeAlarm', { + metric: incomingBytesMetric, + threshold: 5000000, // 5 MB + evaluationPeriods: 1, +}); +``` + ## Patterns Patterns describe which log events match a subscription or metric filter. There diff --git a/packages/aws-cdk-lib/aws-logs/lib/log-group.ts b/packages/aws-cdk-lib/aws-logs/lib/log-group.ts index d1a014ef7cefa..d9ba52ef30ae5 100644 --- a/packages/aws-cdk-lib/aws-logs/lib/log-group.ts +++ b/packages/aws-cdk-lib/aws-logs/lib/log-group.ts @@ -84,6 +84,32 @@ export interface ILogGroup extends iam.IResourceWithPolicy { * Public method to get the physical name of this log group */ logGroupPhysicalName(): string; + + /** + * Return the given named metric for this Log Group + * + * @param metricName The name of the metric + * @param props Properties for the metric + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The number of log events uploaded to CloudWatch Logs. + * When used with the LogGroupName dimension, this is the number of + * log events uploaded to the log group. + * + * @param props Properties for the Cloudwatch metric + */ + metricIncomingLogEvents(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The volume of log events in uncompressed bytes uploaded to CloudWatch Logs. + * When used with the LogGroupName dimension, this is the volume of log events + * in uncompressed bytes uploaded to the log group. + * + * @param props Properties for the Cloudwatch metric + */ + metricIncomingBytes(props?: cloudwatch.MetricOptions): cloudwatch.Metric; } /** @@ -245,6 +271,78 @@ abstract class LogGroupBase extends Resource implements ILogGroup { return principal; } + + /** + * Creates a CloudWatch metric for the number of incoming log events to this log group. + * + * @param props - Optional. Configuration options for the metric. + * @returns A CloudWatch Metric object representing the IncomingLogEvents metric. + * + * This method allows you to monitor the rate at which log events are being ingested + * into the log group. It's useful for understanding the volume of logging activity + * and can help in capacity planning or detecting unusual spikes in logging. + * + * Example usage: + * ``` + * const logGroup = new logs.LogGroup(this, 'MyLogGroup'); + * logGroup.metricIncomingLogEvents().createAlarm(stack, 'IncomingEventsPerInstanceAlarm', { + * threshold: 1, + * evaluationPeriods: 1, + * }); + * ``` + */ + public metricIncomingLogEvents(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IncomingLogs', props); + } + + /** + * Creates a CloudWatch metric for the volume of incoming log data in bytes to this log group. + * + * @param props - Optional. Configuration options for the metric. + * @returns A CloudWatch Metric object representing the IncomingBytes metric. + * + * This method allows you to monitor the volume of data being ingested into the log group. + * It's useful for understanding the size of your logs, which can impact storage costs + * and help in identifying unexpectedly large log entries. + * + * Example usage: + * ``` + * const logGroup = new logs.LogGroup(this, 'MyLogGroup'); + * logGroup.metricIncomingBytes().createAlarm(stack, 'IncomingBytesPerInstanceAlarm', { + * threshold: 1, + * evaluationPeriods: 1, + * }); + * ``` + */ + public metricIncomingBytes(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IncomingBytes', props); + } + + /** + * Creates a CloudWatch metric for this log group. + * + * @param _metricName - The name of the metric to create. + * @param _props - Optional. Additional properties to configure the metric. + * @returns A CloudWatch Metric object representing the specified metric for this log group. + * + * This method creates a CloudWatch Metric object with predefined settings for the log group. + * It sets the namespace to 'AWS/Logs' and the statistic to 'Sum' by default. + * + * The created metric is automatically associated with this log group using the `attachTo` method. + * + * Common metric names for log groups include: + * - 'IncomingBytes': The volume of log data in bytes ingested into the log group. + * - 'IncomingLogEvents': The number of log events ingested into the log group. + * ``` + */ + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/Logs', + metricName, + statistic: 'Sum', + ...props, + }).attachTo(this); + } } /** diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts index 4dc58664dc406..109a447514af2 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts @@ -230,6 +230,10 @@ export class Graph extends GraphNode { return this.children.get(name); } + public containsId(id: string) { + return this.tryGetChild(id) !== undefined; + } + public contains(node: GraphNode) { return this.nodes.has(node); } diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts index cad9fe7769c1a..dbe61b3640dc8 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -134,7 +134,12 @@ export class PipelineGraph { const stackGraphs = new Map(); for (const stack of stage.stacks) { - const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack }); + const stackGraphName = findUniqueName(retGraph, [ + this.simpleStackName(stack.stackName, stage.stageName), + ...stack.account ? [stack.account] : [], + ...stack.region ? [stack.region] : [], + ]); + const stackGraph: AGraph = Graph.of(stackGraphName, { type: 'stack-group', stack }); const prepareNode: AGraphNode | undefined = this.prepareStep ? aGraphNode('Prepare', { type: 'prepare', stack }) : undefined; const deployNode: AGraphNode = aGraphNode('Deploy', { type: 'execute', @@ -412,4 +417,14 @@ function aGraphNode(id: string, x: GraphAnnotation): AGraphNode { function stripPrefix(s: string, prefix: string) { return s.startsWith(prefix) ? s.slice(prefix.length) : s; +} + +function findUniqueName(parent: Graph, parts: string[]): string { + for (let i = 1; i <= parts.length; i++) { + const candidate = parts.slice(0, i).join('.'); + if (!parent.containsId(candidate)) { + return candidate; + } + } + return parts.join('.'); } \ No newline at end of file diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts index 8ca4d83650a8f..2b51dd914c429 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts @@ -81,6 +81,33 @@ test('overridden stack names are respected', () => { }); }); +test('two stacks can have the same name', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { useChangeSets: false }); + pipeline.addStage(new TwoStacksApp(app, 'App')); + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ + { + Name: 'App', + Actions: Match.arrayWith([ + Match.objectLike({ + Name: stringLike('MyFancyStack.Deploy'), + Configuration: Match.objectLike({ + StackName: 'MyFancyStack', + }), + }), + Match.objectLike({ + Name: stringLike('MyFancyStack.eu-west-2.Deploy'), + Configuration: Match.objectLike({ + StackName: 'MyFancyStack', + }), + }), + ]), + }, + ]), + }); +}); + test('changing CLI version leads to a different pipeline structure (restarting it)', () => { // GIVEN @@ -154,3 +181,17 @@ class OneStackAppWithCustomName extends Stage { }); } } + +class TwoStacksApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack1', { + env: { region: 'eu-west-1' }, + stackName: 'MyFancyStack', + }); + new BucketStack(this, 'Stack2', { + env: { region: 'eu-west-2' }, + stackName: 'MyFancyStack', + }); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 81b9ee3fca858..51c0b47a35b0f 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -138,15 +138,11 @@ export class CdkToolkit { const template = deserializeStructure(await fs.readFile(options.templatePath, { encoding: 'UTF-8' })); diffs = options.securityOnly - ? numberFromBool(printSecurityDiff(template, stacks.firstStack, RequireApproval.Broadening, undefined)) - : printStackDiff(template, stacks.firstStack, strict, contextLines, quiet, undefined, false, stream); + ? numberFromBool(printSecurityDiff(template, stacks.firstStack, RequireApproval.Broadening, quiet)) + : printStackDiff(template, stacks.firstStack, strict, contextLines, quiet, undefined, undefined, false, stream); } else { // Compare N stacks against deployed templates for (const stack of stacks.stackArtifacts) { - if (!quiet) { - stream.write(format('Stack %s\n', chalk.bold(stack.displayName))); - } - const templateWithNestedStacks = await this.props.deployments.readCurrentTemplateWithNestedStacks( stack, options.compareAgainstProcessedTemplate, ); @@ -170,7 +166,9 @@ export class CdkToolkit { }); } catch (e: any) { debug(e.message); - stream.write('Checking if the stack exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n'); + if (!quiet) { + stream.write(`Checking if the stack ${stack.stackName} exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n`); + } stackExists = false; } @@ -190,14 +188,12 @@ export class CdkToolkit { } } - if (resourcesToImport) { - stream.write('Parameters and rules created during migration do not affect resource configuration.\n'); - } - const stackCount = options.securityOnly - ? (numberFromBool(printSecurityDiff(currentTemplate, stack, RequireApproval.Broadening, changeSet))) - : (printStackDiff(currentTemplate, stack, strict, contextLines, quiet, changeSet, !!resourcesToImport, stream, nestedStacks)); + ? (numberFromBool(printSecurityDiff(currentTemplate, stack, RequireApproval.Broadening, quiet, stack.displayName, changeSet))) + : (printStackDiff( + currentTemplate, stack, strict, contextLines, quiet, stack.displayName, changeSet, !!resourcesToImport, stream, nestedStacks, + )); diffs += stackCount; } diff --git a/packages/aws-cdk/lib/diff.ts b/packages/aws-cdk/lib/diff.ts index 0e9f1c15543dc..c940efb46fca7 100644 --- a/packages/aws-cdk/lib/diff.ts +++ b/packages/aws-cdk/lib/diff.ts @@ -31,13 +31,22 @@ export function printStackDiff( strict: boolean, context: number, quiet: boolean, + stackName?: string, changeSet?: DescribeChangeSetOutput, isImport?: boolean, stream: FormatStream = process.stderr, nestedStackTemplates?: { [nestedStackLogicalId: string]: NestedStackTemplates }): number { - let diff = fullDiff(oldTemplate, newTemplate.template, changeSet, isImport); + // must output the stack name if there are differences, even if quiet + if (stackName && (!quiet || !diff.isEmpty)) { + stream.write(format('Stack %s\n', chalk.bold(stackName))); + } + + if (!quiet && isImport) { + stream.write('Parameters and rules created during migration do not affect resource configuration.\n'); + } + // detect and filter out mangled characters from the diff let filteredChangesCount = 0; if (diff.differenceCount && !strict) { @@ -74,9 +83,6 @@ export function printStackDiff( break; } const nestedStack = nestedStackTemplates[nestedStackLogicalId]; - if (!quiet) { - stream.write(format('Stack %s\n', chalk.bold(nestedStack.physicalName ?? nestedStackLogicalId))); - } (newTemplate as any)._template = nestedStack.generatedTemplate; stackDiffCount += printStackDiff( @@ -85,6 +91,7 @@ export function printStackDiff( strict, context, quiet, + nestedStack.physicalName ?? nestedStackLogicalId, undefined, isImport, stream, @@ -112,10 +119,18 @@ export function printSecurityDiff( oldTemplate: any, newTemplate: cxapi.CloudFormationStackArtifact, requireApproval: RequireApproval, + quiet?: boolean, + stackName?: string, changeSet?: DescribeChangeSetOutput, + stream: FormatStream = process.stderr, ): boolean { const diff = fullDiff(oldTemplate, newTemplate.template, changeSet); + // must output the stack name if there are differences, even if quiet + if (!quiet || !diff.isEmpty) { + stream.write(format('Stack %s\n', chalk.bold(stackName))); + } + if (difRequiresApproval(diff, requireApproval)) { // eslint-disable-next-line max-len warning(`This deployment will make potentially sensitive changes according to your current security approval level (--require-approval ${requireApproval}).`); diff --git a/packages/aws-cdk/test/diff.test.ts b/packages/aws-cdk/test/diff.test.ts index ffaa157e5fc20..7267f746b1af9 100644 --- a/packages/aws-cdk/test/diff.test.ts +++ b/packages/aws-cdk/test/diff.test.ts @@ -393,15 +393,37 @@ describe('non-nested stacks', () => { // WHEN const exitCode = await toolkit.diff({ - stackNames: ['A', 'A'], + stackNames: ['D'], + stream: buffer, + fail: false, + quiet: true, + }); + + // THEN + const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput).not.toContain('Stack D'); + expect(plainTextOutput).not.toContain('There were no differences'); + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 0'); + expect(exitCode).toBe(0); + }); + + test('when quiet mode is enabled, stacks with diffs should print stack name to stdout', async () => { + // GIVEN + const buffer = new StringWritable(); + + // WHEN + const exitCode = await toolkit.diff({ + stackNames: ['A'], stream: buffer, fail: false, quiet: true, }); // THEN - expect(buffer.data.trim()).not.toContain('Stack A'); - expect(buffer.data.trim()).not.toContain('There were no differences'); + const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput).toContain('Stack A'); + expect(plainTextOutput).not.toContain('There were no differences'); + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1'); expect(exitCode).toBe(0); }); }); @@ -548,10 +570,16 @@ describe('stack exists checks', () => { describe('nested stacks', () => { beforeEach(() => { cloudExecutable = new MockCloudExecutable({ - stacks: [{ - stackName: 'Parent', - template: {}, - }], + stacks: [ + { + stackName: 'Parent', + template: {}, + }, + { + stackName: 'UnchangedParent', + template: {}, + }, + ], }); cloudFormation = instanceMockFrom(Deployments); @@ -718,6 +746,78 @@ describe('nested stacks', () => { }, physicalName: undefined, }, + UnChangedChild: { + deployedTemplate: { + Resources: { + SomeResource: { + Type: 'AWS::Something', + Properties: { + Prop: 'unchanged', + }, + }, + }, + }, + generatedTemplate: { + Resources: { + SomeResource: { + Type: 'AWS::Something', + Properties: { + Prop: 'unchanged', + }, + }, + }, + }, + nestedStackTemplates: {}, + physicalName: 'UnChangedChild', + }, + }, + }); + } + if (stackArtifact.stackName === 'UnchangedParent') { + stackArtifact.template.Resources = { + UnchangedChild: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'child-url', + }, + }, + }; + return Promise.resolve({ + deployedRootTemplate: { + Resources: { + UnchangedChild: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'child-url', + }, + }, + }, + }, + nestedStacks: { + UnchangedChild: { + deployedTemplate: { + Resources: { + SomeResource: { + Type: 'AWS::Something', + Properties: { + Prop: 'unchanged', + }, + }, + }, + }, + generatedTemplate: { + Resources: { + SomeResource: { + Type: 'AWS::Something', + Properties: { + Prop: 'unchanged', + }, + }, + }, + }, + nestedStackTemplates: {}, + physicalName: 'UnchangedChild', + }, }, }); } @@ -784,6 +884,7 @@ Stack newGrandChild Resources [+] AWS::Something SomeResource +Stack UnChangedChild ✨ Number of stacks with differences: 6`); @@ -847,12 +948,95 @@ Stack newGrandChild Resources [+] AWS::Something SomeResource +Stack UnChangedChild ✨ Number of stacks with differences: 6`); expect(exitCode).toBe(0); expect(changeSetSpy).not.toHaveBeenCalled(); }); + + test('when quiet mode is enabled, nested stacks with no diffs should not print stack name & no differences to stdout', async () => { + // GIVEN + const buffer = new StringWritable(); + + // WHEN + const exitCode = await toolkit.diff({ + stackNames: ['UnchangedParent'], + stream: buffer, + fail: false, + quiet: true, + }); + + // THEN + const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/mg, ''); + expect(plainTextOutput).not.toContain('Stack UnchangedParent'); + expect(plainTextOutput).not.toContain('There were no differences'); + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 0'); + expect(exitCode).toBe(0); + }); + + test('when quiet mode is enabled, nested stacks with diffs should print stack name to stdout', async () => { + // GIVEN + const buffer = new StringWritable(); + + // WHEN + const exitCode = await toolkit.diff({ + stackNames: ['Parent'], + stream: buffer, + fail: false, + quiet: true, + }); + + // THEN + const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/mg, ''); + expect(plainTextOutput).toContain(`Stack Parent +Resources +[~] AWS::CloudFormation::Stack AdditionChild + └─ [~] TemplateURL + ├─ [-] addition-child-url-new + └─ [+] addition-child-url-old +[~] AWS::CloudFormation::Stack DeletionChild + └─ [~] TemplateURL + ├─ [-] deletion-child-url-new + └─ [+] deletion-child-url-old +[~] AWS::CloudFormation::Stack ChangedChild + └─ [~] TemplateURL + ├─ [-] changed-child-url-new + └─ [+] changed-child-url-old + +Stack AdditionChild +Resources +[~] AWS::Something SomeResource + └─ [+] Prop + └─ added-value + +Stack DeletionChild +Resources +[~] AWS::Something SomeResource + └─ [-] Prop + └─ value-to-be-removed + +Stack ChangedChild +Resources +[~] AWS::Something SomeResource + └─ [~] Prop + ├─ [-] old-value + └─ [+] new-value + +Stack newChild +Resources +[+] AWS::Something SomeResource + +Stack newGrandChild +Resources +[+] AWS::Something SomeResource + + +✨ Number of stacks with differences: 6`); + expect(plainTextOutput).not.toContain('Stack UnChangedChild'); + expect(exitCode).toBe(0); + }); }); describe('--strict', () => {