Skip to content

Commit

Permalink
feat(lambda): avail function log group in the CDK (#5878)
Browse files Browse the repository at this point in the history
The lambda function's log group is now available for use in the CDK app
so it can be used further to create subscription filters, metric
filters, etc. or in any other way that a regular log group may be used.

The implementation uses an underlying CustomResource to guarantee the
existence of this log group as part of the CloudFormation stack.
However, the custom resource is only created either when the
`logRetention` property is set or when `logGroup` getter is called. This
prevents existing stacks that use Lambda function to get the custom
resource and can be opted into.

closes #3838

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*

<!-- 
Please read the contribution guidelines and follow the pull-request checklist:
https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md
 -->
  • Loading branch information
nija-at authored and mergify[bot] committed Jan 24, 2020
1 parent de3d487 commit fd54a17
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 24 deletions.
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const fn = new lambda.Function(this, 'MyFunction', {
tracing: lambda.Tracing.ACTIVE
});
```

See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html)
to learn more about AWS Lambda's X-Ray support.

Expand All @@ -159,9 +160,28 @@ const fn = new lambda.Function(this, 'MyFunction', {
reservedConcurrentExecutions: 100
});
```

See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html)
managing concurrency.

### Log Group

Lambda functions automatically create a log group with the name `/aws/lambda/<function-name>` upon first execution with
log data set to never expire.

The `logRetention` property can be used to set a different expiration period.

It is possible to obtain the function's log group as a `logs.ILogGroup` by calling the `logGroup` property of the
`Function` construct.

*Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom
resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added
to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the
correct log retention period (never expire, by default).

*Further note* that, if the log group already exists and the `logRetention` is not set, the custom resource will reset
the log retention to never expire even if it was configured with a different value.

### Language-specific APIs
Language-specific higher level constructs are provided in separate modules:

Expand Down
26 changes: 25 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ export class Function extends FunctionBase {

private readonly layers: ILayerVersion[] = [];

private _logGroup?: logs.ILogGroup;

/**
* Environment variables for this function
*/
Expand Down Expand Up @@ -492,11 +494,12 @@ export class Function extends FunctionBase {

// Log retention
if (props.logRetention) {
new LogRetention(this, 'LogRetention', {
const logretention = new LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: props.logRetention,
role: props.logRetentionRole
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logretention.logGroupArn);
}

props.code.bindToResource(resource);
Expand Down Expand Up @@ -576,6 +579,27 @@ export class Function extends FunctionBase {
});
}

/**
* The LogGroup where the Lambda function's logs are made available.
*
* If either `logRetention` is set or this property is called, a CloudFormation custom resource is added to the stack that
* pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the correct log retention
* period (never expire, by default).
*
* Further, if the log group already exists and the `logRetention` is not set, the custom resource will reset the log retention
* to never expire even if it was configured with a different value.
*/
public get logGroup(): logs.ILogGroup {
if (!this._logGroup) {
const logretention = new LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: logs.RetentionDays.INFINITE,
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logretention.logGroupArn);
}
return this._logGroup;
}

private renderEnvironment() {
if (!this.environment || Object.keys(this.environment).length === 0) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: {}
Data: {
// Add log group name as part of the response so that it's available via Fn::GetAtt
LogGroupName: event.ResourceProperties.LogGroupName,
},
});

console.log('Responding', responseBody);
Expand Down
18 changes: 17 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/log-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export interface LogRetentionProps {
* is removed when `retentionDays` is `undefined` or equal to `Infinity`.
*/
export class LogRetention extends cdk.Construct {

/**
* The ARN of the LogGroup.
*/
public readonly logGroupArn: string;

constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) {
super(scope, id);

Expand All @@ -58,13 +64,23 @@ export class LogRetention extends cdk.Construct {

// Need to use a CfnResource here to prevent lerna dependency cycles
// @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation
new cdk.CfnResource(this, 'Resource', {
const resource = new cdk.CfnResource(this, 'Resource', {
type: 'Custom::LogRetention',
properties: {
ServiceToken: provider.functionArn,
LogGroupName: props.logGroupName,
RetentionInDays: props.retention === logs.RetentionDays.INFINITE ? undefined : props.retention
}
});

const logGroupName = resource.getAtt('LogGroupName').toString();
// Append ':*' at the end of the ARN to match with how CloudFormation does this for LogGroup ARNs
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values
this.logGroupArn = cdk.Stack.of(this).formatArn({
service: 'logs',
resource: 'log-group',
resourceName: `${logGroupName}:*`,
sep: ':'
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -146,7 +146,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3"
}
]
}
Expand All @@ -159,7 +159,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3"
}
]
}
Expand Down Expand Up @@ -331,17 +331,17 @@
}
},
"Parameters": {
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300": {
"Type": "String",
"Description": "S3 bucket for asset \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "S3 bucket for asset \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
},
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3": {
"Type": "String",
"Description": "S3 key for asset version \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "S3 key for asset version \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
},
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbArtifactHash1C8D5106": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eArtifactHash8EE34ABC": {
"Type": "String",
"Description": "Artifact hash for asset \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "Artifact hash for asset \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
}
}
}
36 changes: 35 additions & 1 deletion packages/@aws-cdk/aws-lambda/test/test.function.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as logs from '@aws-cdk/aws-logs';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import * as _ from 'lodash';
Expand Down Expand Up @@ -85,5 +86,38 @@ export = testCase({
code: lambda.Code.fromInline('')
}), /Lambda inline code cannot be empty/);
test.done();
}
},

'logGroup is correctly returned'(test: Test) {
const stack = new cdk.Stack();
const fn = new lambda.Function(stack, 'fn', {
handler: 'foo',
runtime: lambda.Runtime.NODEJS_10_X,
code: lambda.Code.fromInline('foo'),
});
const logGroup = fn.logGroup;
test.ok(logGroup.logGroupName);
test.ok(logGroup.logGroupArn);
test.done();
},

'one and only one child LogRetention construct will be created'(test: Test) {
const stack = new cdk.Stack();
const fn = new lambda.Function(stack, 'fn', {
handler: 'foo',
runtime: lambda.Runtime.NODEJS_10_X,
code: lambda.Code.fromInline('foo'),
logRetention: logs.RetentionDays.FIVE_DAYS,
});

// tslint:disable:no-unused-expression
// Call logGroup a few times. If more than one instance of LogRetention was created,
// the second call will fail on duplicate constructs.
fn.logGroup;
fn.logGroup;
fn.logGroup;
// tslint:enable:no-unused-expression

test.done();
},
});
34 changes: 33 additions & 1 deletion packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,37 @@ export = {
test.equal(request.isDone(), true);

test.done();
}
},

async 'response data contains the log group name'(test: Test) {
AWS.mock('CloudWatchLogs', 'createLogGroup', sinon.fake.resolves({}));
AWS.mock('CloudWatchLogs', 'putRetentionPolicy', sinon.fake.resolves({}));
AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', sinon.fake.resolves({}));

const event = {
...eventCommon,
ResourceProperties: {
ServiceToken: 'token',
RetentionInDays: '30',
LogGroupName: 'group'
}
};

async function withOperation(operation: string) {
const request = nock('https://localhost')
.put('/', (body: AWSLambda.CloudFormationCustomResourceResponse) => body.Data?.LogGroupName === 'group')
.reply(200);

const opEvent = { ...event, RequestType: operation };
await provider.handler(opEvent as AWSLambda.CloudFormationCustomResourceCreateEvent, context);

test.equal(request.isDone(), true);
}

await withOperation('Create');
await withOperation('Update');
await withOperation('Delete');

test.done();
},
};
16 changes: 15 additions & 1 deletion packages/@aws-cdk/aws-lambda/test/test.log-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,19 @@ export = {
}));

test.done();
}
},

'log group ARN is well formed and conforms'(test: Test) {
const stack = new cdk.Stack();
const group = new LogRetention(stack, 'MyLambda', {
logGroupName: 'group',
retention: logs.RetentionDays.ONE_MONTH,
});

const logGroupArn = group.logGroupArn;
test.ok(logGroupArn.indexOf('logs') > -1, 'log group ARN is not as expected');
test.ok(logGroupArn.indexOf('log-group') > -1, 'log group ARN is not as expected');
test.ok(logGroupArn.endsWith(':*'), 'log group ARN is not as expected');
test.done();
},
};
18 changes: 9 additions & 9 deletions packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -970,7 +970,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3"
}
]
}
Expand All @@ -983,7 +983,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE"
"Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3"
}
]
}
Expand Down Expand Up @@ -1098,17 +1098,17 @@
}
},
"Parameters": {
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300": {
"Type": "String",
"Description": "S3 bucket for asset \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "S3 bucket for asset \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
},
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3": {
"Type": "String",
"Description": "S3 key for asset version \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "S3 key for asset version \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
},
"AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbArtifactHash1C8D5106": {
"AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eArtifactHash8EE34ABC": {
"Type": "String",
"Description": "Artifact hash for asset \"66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfb\""
"Description": "Artifact hash for asset \"5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0e\""
}
}
}

0 comments on commit fd54a17

Please sign in to comment.