diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts new file mode 100644 index 0000000000000..cd5d7f0d84c54 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts @@ -0,0 +1,40 @@ +import { App, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { LogAnomalyDetector, LogGroup, EvaluationFrequency } from 'aws-cdk-lib/aws-logs'; +// import { LogGroup, EvaluationFrequency } from 'aws-cdk-lib/aws-logs'; + +class TestStack extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const logGroup = new LogGroup(this, 'LogGroup', { + removalPolicy: RemovalPolicy.DESTROY, + }); + + new LogAnomalyDetector(this, 'MyAnomalyDetector', { + detectorName: 'MyDetector', + evaluationFrequency: EvaluationFrequency.FIVE_MIN, + filterPattern: 'ERROR', + logGroupArnList: [logGroup.logGroupArn], + }); + + // const logGroup2 = new LogGroup(this, 'LogGroup2', { + // removalPolicy: RemovalPolicy.DESTROY, + // }); + + // logGroup2.addAnomalyDetector('MyAnomalyDetector2', { + // detectorName: 'MyDetector', + // evaluationFrequency: EvaluationFrequency.TEN_MIN, + // filterPattern: 'WARN', + // }); + + } +} + +const app = new App(); +const testCase = new TestStack(app, 'aws-cdk-anomaly-detector-integ'); + +new IntegTest(app, 'anomaly-detector', { + testCases: [testCase], +}); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.assets.json new file mode 100644 index 0000000000000..01952038f86b9 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.assets.json @@ -0,0 +1,19 @@ +{ + "version": "35.0.0", + "files": { + "3d99811cf4d8b2d453d889e936569b925ead97bdb93a86d122b34d68818be01d": { + "source": { + "path": "aws-cdk-metricfilter-dimensions-integ.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "3d99811cf4d8b2d453d889e936569b925ead97bdb93a86d122b34d68818be01d.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.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.template.json new file mode 100644 index 0000000000000..b2b3588df8f3f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/aws-cdk-metricfilter-dimensions-integ.template.json @@ -0,0 +1,68 @@ +{ + "Resources": { + "LogGroupF5B46931": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MetricFilter1B93B6E5": { + "Type": "AWS::Logs::MetricFilter", + "Properties": { + "FilterPattern": "{ $.latency = \"*\" }", + "LogGroupName": { + "Ref": "LogGroupF5B46931" + }, + "MetricTransformations": [ + { + "Dimensions": [ + { + "Key": "ErrorCode", + "Value": "$.errorCode" + } + ], + "MetricName": "Latency", + "MetricNamespace": "MyApp", + "MetricValue": "$.latency" + } + ] + } + } + }, + "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.anomaly-detector.ts.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/cdk.out new file mode 100644 index 0000000000000..c5cb2e5de6344 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"35.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/integ.json new file mode 100644 index 0000000000000..557b370a7cfc2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "35.0.0", + "testCases": { + "metricfilter-dimensions/DefaultTest": { + "stacks": [ + "aws-cdk-metricfilter-dimensions-integ" + ], + "assertionStack": "metricfilter-dimensions/DefaultTest/DeployAssert", + "assertionStackName": "metricfilterdimensionsDefaultTestDeployAssertF7E39B09" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/manifest.json new file mode 100644 index 0000000000000..928fd0917fb4c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/manifest.json @@ -0,0 +1,119 @@ +{ + "version": "35.0.0", + "artifacts": { + "aws-cdk-metricfilter-dimensions-integ.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-metricfilter-dimensions-integ.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-metricfilter-dimensions-integ": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-metricfilter-dimensions-integ.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "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}/3d99811cf4d8b2d453d889e936569b925ead97bdb93a86d122b34d68818be01d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-metricfilter-dimensions-integ.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-metricfilter-dimensions-integ.assets" + ], + "metadata": { + "/aws-cdk-metricfilter-dimensions-integ/LogGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LogGroupF5B46931" + } + ], + "/aws-cdk-metricfilter-dimensions-integ/MetricFilter/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MetricFilter1B93B6E5" + } + ], + "/aws-cdk-metricfilter-dimensions-integ/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-metricfilter-dimensions-integ/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-metricfilter-dimensions-integ" + }, + "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "metricfilterdimensionsDefaultTestDeployAssertF7E39B09": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "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": [ + "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.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": [ + "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets" + ], + "metadata": { + "/metricfilter-dimensions/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/metricfilter-dimensions/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "metricfilter-dimensions/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.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets.json new file mode 100644 index 0000000000000..5f3b21b660549 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.assets.json @@ -0,0 +1,19 @@ +{ + "version": "35.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "metricfilterdimensionsDefaultTestDeployAssertF7E39B09.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.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/metricfilterdimensionsDefaultTestDeployAssertF7E39B09.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.anomaly-detector.ts.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/tree.json new file mode 100644 index 0000000000000..a5ee4e900c93a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-logs/test/integ.anomaly-detector.ts.snapshot/tree.json @@ -0,0 +1,165 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-metricfilter-dimensions-integ": { + "id": "aws-cdk-metricfilter-dimensions-integ", + "path": "aws-cdk-metricfilter-dimensions-integ", + "children": { + "LogGroup": { + "id": "LogGroup", + "path": "aws-cdk-metricfilter-dimensions-integ/LogGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-metricfilter-dimensions-integ/LogGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Logs::LogGroup", + "aws:cdk:cloudformation:props": { + "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" + } + }, + "MetricFilter": { + "id": "MetricFilter", + "path": "aws-cdk-metricfilter-dimensions-integ/MetricFilter", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-metricfilter-dimensions-integ/MetricFilter/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Logs::MetricFilter", + "aws:cdk:cloudformation:props": { + "filterPattern": "{ $.latency = \"*\" }", + "logGroupName": { + "Ref": "LogGroupF5B46931" + }, + "metricTransformations": [ + { + "metricNamespace": "MyApp", + "metricName": "Latency", + "metricValue": "$.latency", + "dimensions": [ + { + "key": "ErrorCode", + "value": "$.errorCode" + } + ] + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_logs.CfnMetricFilter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_logs.MetricFilter", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-metricfilter-dimensions-integ/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-metricfilter-dimensions-integ/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "metricfilter-dimensions": { + "id": "metricfilter-dimensions", + "path": "metricfilter-dimensions", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "metricfilter-dimensions/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "metricfilter-dimensions/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "metricfilter-dimensions/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "metricfilter-dimensions/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "metricfilter-dimensions/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-lib/aws-logs/README.md b/packages/aws-cdk-lib/aws-logs/README.md index a4772aac170cb..f3d9b2c194bd4 100644 --- a/packages/aws-cdk-lib/aws-logs/README.md +++ b/packages/aws-cdk-lib/aws-logs/README.md @@ -380,6 +380,35 @@ new logs.LogGroup(this, 'LogGroupLambda', { }); ``` +## Log Anomaly Detector + +The LogAnomalyDetector construct allows you to create and configure an anomaly detector for CloudWatch Log Groups. + +For more information, see [Log anomaly detection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/LogsAnomalyDetection.html) + +```ts +const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + +new logs.LogAnomalyDetector(stack, 'MyAnomalyDetector', { + [logGroup], + detectorName: 'MyDetector', + evaluationFrequency: logs.EvaluationFrequency.FIVE_MIN, + filterPattern: 'ERROR', +}); +``` + +Adding Anomaly Detector to Existing Log Group +You can add an anomaly detector directly to an existing log group using the addAnomalyDetector method. + +```ts +const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); +logGroup.addAnomalyDetector('MyAnomalyDetector', { + detectorName: 'MyDetector', + evaluationFrequency: logs.EvaluationFrequency.TEN_MIN, + filterPattern: 'WARN', +}); +``` + ## Notes Be aware that Log Group ARNs will always have the string `:*` appended to diff --git a/packages/aws-cdk-lib/aws-logs/lib/index.ts b/packages/aws-cdk-lib/aws-logs/lib/index.ts index 71f2717cc4447..e0e5581b2b1bc 100644 --- a/packages/aws-cdk-lib/aws-logs/lib/index.ts +++ b/packages/aws-cdk-lib/aws-logs/lib/index.ts @@ -1,4 +1,5 @@ export * from './cross-account-destination'; +export * from './log-anomaly-detector'; export * from './log-group'; export * from './log-stream'; export * from './metric-filter'; diff --git a/packages/aws-cdk-lib/aws-logs/lib/log-anomaly-detector.ts b/packages/aws-cdk-lib/aws-logs/lib/log-anomaly-detector.ts new file mode 100644 index 0000000000000..b9d186a62d0c4 --- /dev/null +++ b/packages/aws-cdk-lib/aws-logs/lib/log-anomaly-detector.ts @@ -0,0 +1,113 @@ +import { Construct } from 'constructs'; +import { CfnLogAnomalyDetector } from './logs.generated'; +import { Arn, ArnFormat, Resource } from '../../core'; + +export enum EvaluationFrequency { + /** + * FIFTEEN_MIN + */ + FIFTEEN_MIN = 'FIFTEEN_MIN', + /** + * FIVE_MIN + */ + FIVE_MIN = 'FIVE_MIN', + /** + * ONE_HOUR + */ + ONE_HOUR = 'ONE_HOUR', + /** + * TEN_MIN + */ + TEN_MIN = 'TEN_MIN', + /** + * THIRTY_MIN + */ + THIRTY_MIN = 'THIRTY_MIN', +} + +export interface LogAnomalyDetectorOptions { + /** + * The ID of the account to create the anomaly detector in. + * If not specified, the current account is used. + */ + readonly accountId?: string; + + /** + * The number of days to have visibility on an anomaly. + * After this period, the anomaly is automatically baselined. + */ + readonly anomalyVisibilityTime?: number; + + /** + * A name for this anomaly detector. + */ + readonly detectorName?: string; + + /** + * Specifies how often the anomaly detector is to run. + * Choose from the EvaluationFrequency enum. + */ + readonly evaluationFrequency?: EvaluationFrequency; + + /** + * Pattern to limit the anomaly detection model to examine only log events that match. + */ + readonly filterPattern?: string; + + /** + * Optionally assigns a AWS KMS key to secure this anomaly detector and its findings. + */ + readonly kmsKeyId?: string; + + /** + * The ARN of the log group that is associated with this anomaly detector. + * You can specify only one log group ARN. + */ + readonly logGroupArnList?: string[]; +} + +/** + * A detector that identifies anomalies in CloudWatch Log Groups and reports them. + */ +export class LogAnomalyDetector extends Resource { + + /** + * Constructs a new instance of the LogAnomalyDetector class. + * + * @param scope The scope in which to define this construct. + * @param id The scoped construct ID. Must be unique amongst siblings in the same scope. + * @param props The properties for configuring the LogAnomalyDetector. + */ + constructor(scope: Construct, id: string, props: LogAnomalyDetectorOptions) { + super(scope, id, { + physicalName: props.detectorName, + }); + + // Validate the logGroupArnList if provided + if (!props.logGroupArnList || props.logGroupArnList.length === 0) { + throw new Error('logGroupArnList must be provided and cannot be empty.'); + } + + // Parse and reconstruct each ARN in the list + const parsedArnList = props.logGroupArnList.map(arn => { + const parsedArn = Arn.split(arn, ArnFormat.COLON_RESOURCE_NAME); + return `arn:${parsedArn.partition}:${parsedArn.region}:${parsedArn.account}:${parsedArn.resource}:${parsedArn.resourceName}`; + }); + + const parsedArnList = props.logGroupArnList.map(arn => { + const parsedArn = Arn.parse(arn); + return `arn:${parsedArn.partition}:${parsedArn.service}:${parsedArn.region}:${parsedArn.account}:${parsedArn.resource}`; + }); + + // Create the CloudFormation resource for the Log Anomaly Detector + new CfnLogAnomalyDetector(this, id, { + accountId: props.accountId, + anomalyVisibilityTime: props.anomalyVisibilityTime, + detectorName: props.detectorName ?? id, + evaluationFrequency: props.evaluationFrequency, + filterPattern: props.filterPattern, + kmsKeyId: props.kmsKeyId, + logGroupArnList: parsedArnList, + }); + } +} \ No newline at end of file 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 6f67bcc6161f0..32598e656a05c 100644 --- a/packages/aws-cdk-lib/aws-logs/lib/log-group.ts +++ b/packages/aws-cdk-lib/aws-logs/lib/log-group.ts @@ -1,5 +1,6 @@ import { Construct } from 'constructs'; import { DataProtectionPolicy } from './data-protection-policy'; +import { LogAnomalyDetector, LogAnomalyDetectorOptions } from './log-anomaly-detector'; import { LogStream } from './log-stream'; import { CfnLogGroup } from './logs.generated'; import { MetricFilter } from './metric-filter'; @@ -49,6 +50,14 @@ export interface ILogGroup extends iam.IResourceWithPolicy { */ addMetricFilter(id: string, props: MetricFilterOptions): MetricFilter; + /** + * Create a new Anomaly Detector for this Log Group + * + * @param id Unique identifier for the construct in its parent + * @param props Properties for creating the LogStream + */ + addAnomalyDetector(id: string, props?: LogAnomalyDetectorOptions): LogAnomalyDetector; + /** * Extract a metric from structured log events in the LogGroup * @@ -141,6 +150,26 @@ abstract class LogGroupBase extends Resource implements ILogGroup { }); } + /** + * Create a new Log Anomaly Detector for this Log Group + * + * @param id Unique identifier for the construct in its parent + * @param props Properties for creating the LogAnomalyDetector + * @returns The created LogAnomalyDetector instance + */ + public addAnomalyDetector(id: string, props: LogAnomalyDetectorOptions): LogAnomalyDetector { + // Ensure the log group ARN is included in the logGroupArnList + const logGroupArnList = props.logGroupArnList ?? []; + const logGroupArn = this.logGroupArn; + if (!logGroupArnList.includes(logGroupArn)) { + logGroupArnList.push(logGroupArn); + } + + return new LogAnomalyDetector(this, id, { + ...props, + logGroupArnList: logGroupArnList, + }); + } /** * Extract a metric from structured log events in the LogGroup * @@ -235,7 +264,6 @@ abstract class LogGroupBase extends Resource implements ILogGroup { // ARN created by this call. return new iam.ArnPrincipal(principal.principalAccount); } - if (principal instanceof iam.ArnPrincipal) { const parsedArn = Arn.split(principal.arn, ArnFormat.SLASH_RESOURCE_NAME); if (parsedArn.account) { diff --git a/packages/aws-cdk-lib/aws-logs/test/anomalydetector.test.ts b/packages/aws-cdk-lib/aws-logs/test/anomalydetector.test.ts new file mode 100644 index 0000000000000..31291fff8d1ed --- /dev/null +++ b/packages/aws-cdk-lib/aws-logs/test/anomalydetector.test.ts @@ -0,0 +1,74 @@ +import { Template } from '../../assertions'; +import { Stack } from '../../core'; +import { LogGroup, LogAnomalyDetector, EvaluationFrequency } from '../lib'; // Adjust the import path as necessary + +describe('LogAnomalyDetector', () => { + test('basic instantiation', () => { + // GIVEN + const stack = new Stack(); + const logGroup = new LogGroup(stack, 'TestLogGroup'); + + // WHEN + new LogAnomalyDetector(stack, 'TestAnomalyDetector', { + detectorName: 'TestDetector', + evaluationFrequency: EvaluationFrequency.FIVE_MIN, + filterPattern: 'ERROR', + logGroupArnList: [logGroup.logGroupArn], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogAnomalyDetector', { + DetectorName: 'TestDetector', + EvaluationFrequency: 'FIVE_MIN', + FilterPattern: 'ERROR', + LogGroupArnList: [{ 'Fn::GetAtt': ['TestLogGroup4EEF7AD4', 'Arn'] }], + }); + }); + + test('anomaly detector with optional properties', () => { + // GIVEN + const stack = new Stack(); + const logGroup = new LogGroup(stack, 'TestLogGroup'); + + // WHEN + new LogAnomalyDetector(stack, 'TestAnomalyDetector', { + detectorName: 'TestDetector', + evaluationFrequency: EvaluationFrequency.ONE_HOUR, + filterPattern: 'WARNING', + anomalyVisibilityTime: 5, + kmsKeyId: 'test-kms-key-id', + logGroupArnList: [logGroup.logGroupArn], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogAnomalyDetector', { + DetectorName: 'TestDetector', + EvaluationFrequency: 'ONE_HOUR', + FilterPattern: 'WARNING', + AnomalyVisibilityTime: 5, + KmsKeyId: 'test-kms-key-id', + LogGroupArnList: [{ 'Fn::GetAtt': ['TestLogGroup4EEF7AD4', 'Arn'] }], + }); + }); + + test('addAnomalyDetector', () => { + // GIVEN + const stack = new Stack(); + const logGroup = new LogGroup(stack, 'TestLogGroup'); + + // WHEN + logGroup.addAnomalyDetector('TestAnomalyDetector', { + detectorName: 'TestDetector', + evaluationFrequency: EvaluationFrequency.FIVE_MIN, + filterPattern: 'ERROR', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogAnomalyDetector', { + DetectorName: 'TestDetector', + EvaluationFrequency: 'FIVE_MIN', + FilterPattern: 'ERROR', + }); + }); +}); +