Skip to content

Commit 4dbf5c8

Browse files
authored
fix(cli): cdk watch for Lambdas with Advanced Logging Controls do not stream logs to the terminal (#29451)
### Issue Closes #29448 ### Reason for this change After the release of Advanced Logging Controls for Lambda (see https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) I've decided to move all the logs of my multi-stack deployment to a single unified Log Group per deployed tenant. It goes likes this: The main stack creates the LogGroup like `/aws/lambda/{tenant}`; The secondary stacks refers to the LogGroup using `LogGroup.fromLogGroupArn(...)`; Then I discovered that running cdk watch on the main stack I can see the cloudwatch logs on my terminal while on the secondary stacks it does not; I found at that the cloudwatch log group resolver for the logs basically just assumes the log group is `/aws/lambda/{physicalId}` https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts#L114 ### Description of changes Use the Template JSON to resolve the `LoggingConfig.LogGroupName` of the Lambda Function; ### Description of how you validated changes I've replace the node_modules/aws-cdk of my project with the one built from this PR and I was able to see the logs of my lambda that has custom LogConfig, as you can see it is grabbing the logs of `/aws/lambda/dev` cloudwatch logs ![2024-03-13T10-46-48](https://github.com/aws/aws-cdk/assets/980905/d12113f6-67b6-4a54-96cf-66df6138d7f7) ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)
1 parent 2cc2449 commit 4dbf5c8

File tree

3 files changed

+117
-33
lines changed

3 files changed

+117
-33
lines changed

packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts

+4
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ export class EvaluateCloudFormationTemplate {
314314
return cfnExpression;
315315
}
316316

317+
public getResourceProperty(logicalId: string, propertyName: string): any {
318+
return this.template.Resources?.[logicalId]?.Properties?.[propertyName];
319+
}
320+
317321
private references(logicalId: string, templateElement: any): boolean {
318322
if (typeof templateElement === 'string') {
319323
return logicalId === templateElement;

packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts

+53-33
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import * as cxapi from '@aws-cdk/cx-api';
22
import { CloudFormation } from 'aws-sdk';
3-
import { Mode, SdkProvider, ISDK } from '../aws-auth';
3+
import { ISDK, Mode, SdkProvider } from '../aws-auth';
44
import { Deployments } from '../deployments';
55
import { EvaluateCloudFormationTemplate, LazyListStackResources } from '../evaluate-cloudformation-template';
66

77
// resource types that have associated CloudWatch Log Groups that should _not_ be monitored
88
const IGNORE_LOGS_RESOURCE_TYPES = ['AWS::EC2::FlowLog', 'AWS::CloudTrail::Trail', 'AWS::CodeBuild::Project'];
99

10-
// Resource types that will create a CloudWatch log group with a specific name if one is not provided.
11-
// The keys are CFN resource types, and the values are the name of the physical name property of that resource
12-
// and the service name that is used in the automatically created CloudWatch log group.
13-
const RESOURCE_TYPES_WITH_IMPLICIT_LOGS: { [cfnResourceType: string]: { [key: string]: string } } = {
14-
'AWS::Lambda::Function': {
15-
PhysicalNamePropertyName: 'FunctionName',
16-
LogGroupServiceName: 'lambda',
17-
},
18-
};
19-
2010
/**
2111
* Configuration needed to monitor CloudWatch Log Groups
2212
* found in a given CloudFormation Stack
@@ -84,38 +74,68 @@ function isReferencedFromIgnoredResource(
8474
logGroupResource: CloudFormation.StackResourceSummary,
8575
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
8676
): boolean {
87-
let foundReference = false;
8877
const resourcesReferencingLogGroup = evaluateCfnTemplate.findReferencesTo(logGroupResource.LogicalResourceId);
89-
for (const reference of resourcesReferencingLogGroup) {
90-
if (IGNORE_LOGS_RESOURCE_TYPES.includes(reference.Type)) {
91-
foundReference = true;
92-
}
93-
}
94-
return foundReference;
78+
return resourcesReferencingLogGroup.some(reference => {
79+
return IGNORE_LOGS_RESOURCE_TYPES.includes(reference.Type);
80+
});
9581
}
9682

83+
type CloudWatchLogsResolver = (
84+
resource: CloudFormation.StackResourceSummary,
85+
evaluateCfnTemplate: EvaluateCloudFormationTemplate
86+
) => string | undefined;
87+
88+
const cloudWatchLogsResolvers: Record<string, CloudWatchLogsResolver> = {
89+
'AWS::Logs::LogGroup': (resource, evaluateCfnTemplate) => {
90+
if (isReferencedFromIgnoredResource(resource, evaluateCfnTemplate)) {
91+
return undefined;
92+
}
93+
return resource.PhysicalResourceId?.toString();
94+
},
95+
96+
// Resource types that will create a CloudWatch log group with a specific name if one is not provided.
97+
// The keys are CFN resource types, and the values are the name of the physical name property of that resource
98+
// and the service name that is used in the automatically created CloudWatch log group.
99+
'AWS::Lambda::Function': (resource, evaluateCfnTemplate) => {
100+
const loggingConfig = evaluateCfnTemplate.getResourceProperty(resource.LogicalResourceId, 'LoggingConfig');
101+
if (loggingConfig?.LogGroup) {
102+
// if LogGroup is a string then use it as the LogGroupName as it is referred by LogGroup.fromLogGroupArn in CDK
103+
if (typeof loggingConfig.LogGroup === 'string') {
104+
return loggingConfig.LogGroup;
105+
}
106+
107+
// if { Ref: '...' } is used then try to resolve the LogGroupName from the referenced resource in the template
108+
if (typeof loggingConfig.LogGroup === 'object') {
109+
if (loggingConfig.LogGroup.Ref) {
110+
return evaluateCfnTemplate.getResourceProperty(loggingConfig.LogGroup.Ref, 'LogGroupName');
111+
}
112+
}
113+
}
114+
115+
return `/aws/lambda/${resource.PhysicalResourceId}`;
116+
},
117+
};
118+
97119
/**
98120
* Find all CloudWatch Log Groups in the deployed template.
99-
* This will find both explicitely created Log Groups (excluding those associated with ignored resources)
100-
* as well as Log Groups created implicitely (i.e. Lambda Functions)
121+
* This will find both explicitly created Log Groups (excluding those associated with ignored resources)
122+
* and Log Groups created implicitly (i.e. Lambda Functions)
101123
*/
102124
function findAllLogGroupNames(
103125
stackResources: CloudFormation.StackResourceSummary[],
104126
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
105127
): string[] {
106-
return stackResources.reduce((logGroupNames: string[], resource) => {
107-
let logGroupName;
108-
if (resource.ResourceType === 'AWS::Logs::LogGroup') {
109-
if (!isReferencedFromIgnoredResource(resource, evaluateCfnTemplate)) {
110-
logGroupName = resource.PhysicalResourceId;
128+
const logGroupNames: string[] = [];
129+
130+
for (const resource of stackResources) {
131+
const logGroupResolver = cloudWatchLogsResolvers[resource.ResourceType];
132+
if (logGroupResolver) {
133+
const logGroupName = logGroupResolver(resource, evaluateCfnTemplate);
134+
if (logGroupName) {
135+
logGroupNames.push(logGroupName);
111136
}
112-
} else if (RESOURCE_TYPES_WITH_IMPLICIT_LOGS[resource.ResourceType]) {
113-
const servicePart = RESOURCE_TYPES_WITH_IMPLICIT_LOGS[resource.ResourceType].LogGroupServiceName;
114-
logGroupName = `/aws/${servicePart}/${resource.PhysicalResourceId}`;
115-
}
116-
if (logGroupName) {
117-
logGroupNames.push(logGroupName);
118137
}
119-
return logGroupNames;
120-
}, []);
138+
}
139+
140+
return logGroupNames;
121141
}

packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,66 @@ test('add log groups from lambda function', async () => {
3939
expect(result.logGroupNames).toEqual(['/aws/lambda/my-function']);
4040
});
4141

42+
test('add log groups from lambda function when using custom LoggingConfig', async () => {
43+
// GIVEN
44+
const cdkStackArtifact = cdkStackArtifactOf({
45+
template: {
46+
Resources: {
47+
Func: {
48+
Type: 'AWS::Lambda::Function',
49+
Properties: {
50+
FunctionName: 'my-function',
51+
LoggingConfig: {
52+
LogGroup: '/this/custom/my-custom-log-group',
53+
},
54+
},
55+
},
56+
},
57+
},
58+
});
59+
pushStackResourceSummaries(stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function'));
60+
61+
// WHEN
62+
const result = await findCloudWatchLogGroups(logsMockSdkProvider.mockSdkProvider, cdkStackArtifact);
63+
64+
// THEN
65+
expect(result.logGroupNames).toEqual(['/this/custom/my-custom-log-group']);
66+
});
67+
68+
test('add log groups from lambda function when using custom LoggingConfig using Ref', async () => {
69+
// GIVEN
70+
const cdkStackArtifact = cdkStackArtifactOf({
71+
template: {
72+
Resources: {
73+
MyCustomLogGroupLogicalId: {
74+
Type: 'AWS::Logs::LogGroup',
75+
Properties: {
76+
LogGroupName: '/this/custom/my-custom-log-group',
77+
},
78+
},
79+
Func: {
80+
Type: 'AWS::Lambda::Function',
81+
Properties: {
82+
FunctionName: 'my-function',
83+
LoggingConfig: {
84+
LogGroup: {
85+
Ref: 'MyCustomLogGroupLogicalId',
86+
},
87+
},
88+
},
89+
},
90+
},
91+
},
92+
});
93+
pushStackResourceSummaries(stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function'));
94+
95+
// WHEN
96+
const result = await findCloudWatchLogGroups(logsMockSdkProvider.mockSdkProvider, cdkStackArtifact);
97+
98+
// THEN
99+
expect(result.logGroupNames).toEqual(['/this/custom/my-custom-log-group']);
100+
});
101+
42102
test('add log groups from lambda function without physical name', async () => {
43103
// GIVEN
44104
const cdkStackArtifact = cdkStackArtifactOf({

0 commit comments

Comments
 (0)