From fd54a17a82605ac1301e5776aa68f03bbfb63910 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar <16217941+nija-at@users.noreply.github.com> Date: Fri, 24 Jan 2020 16:36:51 +0000 Subject: [PATCH] feat(lambda): avail function log group in the CDK (#5878) 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* --- packages/@aws-cdk/aws-lambda/README.md | 20 +++++++++++ packages/@aws-cdk/aws-lambda/lib/function.ts | 26 +++++++++++++- .../lib/log-retention-provider/index.ts | 5 ++- .../@aws-cdk/aws-lambda/lib/log-retention.ts | 18 +++++++++- .../test/integ.log-retention.expected.json | 18 +++++----- .../@aws-cdk/aws-lambda/test/test.function.ts | 36 ++++++++++++++++++- .../test/test.log-retention-provider.ts | 34 +++++++++++++++++- .../aws-lambda/test/test.log-retention.ts | 16 ++++++++- .../test/integ.instance.lit.expected.json | 18 +++++----- 9 files changed, 167 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 87526d19f7cff..bf63e94b867f1 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -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. @@ -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/` 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: diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 1967ec955904b..db0c922ef6bba 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -413,6 +413,8 @@ export class Function extends FunctionBase { private readonly layers: ILayerVersion[] = []; + private _logGroup?: logs.ILogGroup; + /** * Environment variables for this function */ @@ -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); @@ -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; diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts index 8ebc6503149cd..e25eba7a25820 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts @@ -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); diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts index 48d4e4fd7f877..9be651d2e0989 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts @@ -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); @@ -58,7 +64,7 @@ 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, @@ -66,5 +72,15 @@ export class LogRetention extends cdk.Construct { 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: ':' + }); } } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json index 95cf3144afa23..5b7b8845f5013 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -133,7 +133,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300" }, "S3Key": { "Fn::Join": [ @@ -146,7 +146,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3" } ] } @@ -159,7 +159,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3" } ] } @@ -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\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/test.function.ts b/packages/@aws-cdk/aws-lambda/test/test.function.ts index 1a74e2adcb9b3..5134a5fc69287 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.function.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.function.ts @@ -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'; @@ -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(); + }, }); diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts index 89c5330442844..def4167afd554 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts @@ -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(); + }, }; diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts index 8683b2374d255..845f59c0341ab 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts @@ -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(); + }, }; diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index 7845f40ddb0e4..ef7ba06305601 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -957,7 +957,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3Bucket26B8E770" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3BucketFCE6B300" }, "S3Key": { "Fn::Join": [ @@ -970,7 +970,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3" } ] } @@ -983,7 +983,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters66b36b09a60a9569c7e68ece07c423f900fda09bfebf230559a4dc2b24562cfbS3VersionKey8B860BBE" + "Ref": "AssetParameters5c8f562bf5df6762658daef5776a5710bf97377b5129c14ef98dad430a546e0eS3VersionKey98D205C3" } ] } @@ -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\"" } } }