Skip to content

Commit

Permalink
fix(cli): cdk watch for Lambdas with Advanced Logging Controls do not…
Browse files Browse the repository at this point in the history
… 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)
  • Loading branch information
onhate authored Mar 27, 2024
1 parent 2cc2449 commit 4dbf5c8
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 33 deletions.
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
86 changes: 53 additions & 33 deletions packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string, CloudWatchLogsResolver> = {
'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;
}
60 changes: 60 additions & 0 deletions packages/aws-cdk/test/api/logs/find-cloudwatch-logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 4dbf5c8

Please sign in to comment.