diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index 5a1cc675bff85..088e40b28f3b4 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -314,6 +314,10 @@ export class EvaluateCloudFormationTemplate { return cfnExpression; } + public getResourceProperty(logicalId: string, propertyName: string): any { + return this.template.Resources?.[logicalId]?.Properties?.[propertyName]; + } + private references(logicalId: string, templateElement: any): boolean { if (typeof templateElement === 'string') { return logicalId === templateElement; diff --git a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts index bfff256221a52..f56ed70d0746b 100644 --- a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts +++ b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts @@ -1,22 +1,12 @@ import * as cxapi from '@aws-cdk/cx-api'; import { CloudFormation } from 'aws-sdk'; -import { Mode, SdkProvider, ISDK } from '../aws-auth'; +import { ISDK, Mode, SdkProvider } from '../aws-auth'; import { Deployments } from '../deployments'; import { EvaluateCloudFormationTemplate, LazyListStackResources } from '../evaluate-cloudformation-template'; // resource types that have associated CloudWatch Log Groups that should _not_ be monitored const IGNORE_LOGS_RESOURCE_TYPES = ['AWS::EC2::FlowLog', 'AWS::CloudTrail::Trail', 'AWS::CodeBuild::Project']; -// Resource types that will create a CloudWatch log group with a specific name if one is not provided. -// The keys are CFN resource types, and the values are the name of the physical name property of that resource -// and the service name that is used in the automatically created CloudWatch log group. -const RESOURCE_TYPES_WITH_IMPLICIT_LOGS: { [cfnResourceType: string]: { [key: string]: string } } = { - 'AWS::Lambda::Function': { - PhysicalNamePropertyName: 'FunctionName', - LogGroupServiceName: 'lambda', - }, -}; - /** * Configuration needed to monitor CloudWatch Log Groups * found in a given CloudFormation Stack @@ -84,38 +74,68 @@ function isReferencedFromIgnoredResource( logGroupResource: CloudFormation.StackResourceSummary, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): boolean { - let foundReference = false; const resourcesReferencingLogGroup = evaluateCfnTemplate.findReferencesTo(logGroupResource.LogicalResourceId); - for (const reference of resourcesReferencingLogGroup) { - if (IGNORE_LOGS_RESOURCE_TYPES.includes(reference.Type)) { - foundReference = true; - } - } - return foundReference; + return resourcesReferencingLogGroup.some(reference => { + return IGNORE_LOGS_RESOURCE_TYPES.includes(reference.Type); + }); } +type CloudWatchLogsResolver = ( + resource: CloudFormation.StackResourceSummary, + evaluateCfnTemplate: EvaluateCloudFormationTemplate +) => string | undefined; + +const cloudWatchLogsResolvers: Record = { + 'AWS::Logs::LogGroup': (resource, evaluateCfnTemplate) => { + if (isReferencedFromIgnoredResource(resource, evaluateCfnTemplate)) { + return undefined; + } + return resource.PhysicalResourceId?.toString(); + }, + + // Resource types that will create a CloudWatch log group with a specific name if one is not provided. + // The keys are CFN resource types, and the values are the name of the physical name property of that resource + // and the service name that is used in the automatically created CloudWatch log group. + 'AWS::Lambda::Function': (resource, evaluateCfnTemplate) => { + const loggingConfig = evaluateCfnTemplate.getResourceProperty(resource.LogicalResourceId, 'LoggingConfig'); + if (loggingConfig?.LogGroup) { + // if LogGroup is a string then use it as the LogGroupName as it is referred by LogGroup.fromLogGroupArn in CDK + if (typeof loggingConfig.LogGroup === 'string') { + return loggingConfig.LogGroup; + } + + // if { Ref: '...' } is used then try to resolve the LogGroupName from the referenced resource in the template + if (typeof loggingConfig.LogGroup === 'object') { + if (loggingConfig.LogGroup.Ref) { + return evaluateCfnTemplate.getResourceProperty(loggingConfig.LogGroup.Ref, 'LogGroupName'); + } + } + } + + return `/aws/lambda/${resource.PhysicalResourceId}`; + }, +}; + /** * Find all CloudWatch Log Groups in the deployed template. - * This will find both explicitely created Log Groups (excluding those associated with ignored resources) - * as well as Log Groups created implicitely (i.e. Lambda Functions) + * This will find both explicitly created Log Groups (excluding those associated with ignored resources) + * and Log Groups created implicitly (i.e. Lambda Functions) */ function findAllLogGroupNames( stackResources: CloudFormation.StackResourceSummary[], evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): string[] { - return stackResources.reduce((logGroupNames: string[], resource) => { - let logGroupName; - if (resource.ResourceType === 'AWS::Logs::LogGroup') { - if (!isReferencedFromIgnoredResource(resource, evaluateCfnTemplate)) { - logGroupName = resource.PhysicalResourceId; + const logGroupNames: string[] = []; + + for (const resource of stackResources) { + const logGroupResolver = cloudWatchLogsResolvers[resource.ResourceType]; + if (logGroupResolver) { + const logGroupName = logGroupResolver(resource, evaluateCfnTemplate); + if (logGroupName) { + logGroupNames.push(logGroupName); } - } else if (RESOURCE_TYPES_WITH_IMPLICIT_LOGS[resource.ResourceType]) { - const servicePart = RESOURCE_TYPES_WITH_IMPLICIT_LOGS[resource.ResourceType].LogGroupServiceName; - logGroupName = `/aws/${servicePart}/${resource.PhysicalResourceId}`; - } - if (logGroupName) { - logGroupNames.push(logGroupName); } - return logGroupNames; - }, []); + } + + return logGroupNames; } diff --git a/packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts b/packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts index 6e693fee10477..c54eccd73ed47 100644 --- a/packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts +++ b/packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts @@ -39,6 +39,66 @@ test('add log groups from lambda function', async () => { expect(result.logGroupNames).toEqual(['/aws/lambda/my-function']); }); +test('add log groups from lambda function when using custom LoggingConfig', async () => { + // GIVEN + const cdkStackArtifact = cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + FunctionName: 'my-function', + LoggingConfig: { + LogGroup: '/this/custom/my-custom-log-group', + }, + }, + }, + }, + }, + }); + pushStackResourceSummaries(stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function')); + + // WHEN + const result = await findCloudWatchLogGroups(logsMockSdkProvider.mockSdkProvider, cdkStackArtifact); + + // THEN + expect(result.logGroupNames).toEqual(['/this/custom/my-custom-log-group']); +}); + +test('add log groups from lambda function when using custom LoggingConfig using Ref', async () => { + // GIVEN + const cdkStackArtifact = cdkStackArtifactOf({ + template: { + Resources: { + MyCustomLogGroupLogicalId: { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: '/this/custom/my-custom-log-group', + }, + }, + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + FunctionName: 'my-function', + LoggingConfig: { + LogGroup: { + Ref: 'MyCustomLogGroupLogicalId', + }, + }, + }, + }, + }, + }, + }); + pushStackResourceSummaries(stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function')); + + // WHEN + const result = await findCloudWatchLogGroups(logsMockSdkProvider.mockSdkProvider, cdkStackArtifact); + + // THEN + expect(result.logGroupNames).toEqual(['/this/custom/my-custom-log-group']); +}); + test('add log groups from lambda function without physical name', async () => { // GIVEN const cdkStackArtifact = cdkStackArtifactOf({