Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(logs): generic service managed log group to replace log retention #26947

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions packages/aws-cdk-lib/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
* this property, unsetting it doesn't remove the log retention policy. To
* remove the retention policy, set the value to `INFINITE`.
*
* @deprecated Use `logGroup` instead
* @default logs.RetentionDays.INFINITE
*/
readonly logRetention?: logs.RetentionDays;
Expand All @@ -318,6 +319,7 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
* The IAM role for the Lambda function associated with the custom resource
* that sets the retention policy.
*
* @deprecated Use `logGroup` instead
* @default - A new role is created.
*/
readonly logRetentionRole?: iam.IRole;
Expand All @@ -326,6 +328,7 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
* When log retention is specified, a custom resource attempts to create the CloudWatch log group.
* These options control the retry policy when interacting with CloudWatch APIs.
*
* @deprecated Use `logGroup` instead
* @default - Default AWS SDK retry options.
*/
readonly logRetentionRetryOptions?: LogRetentionRetryOptions;
Expand Down Expand Up @@ -385,6 +388,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
* @default Auto
*/
readonly runtimeManagementMode?: RuntimeManagementMode;

/**
* Configure the log group of the lambda function
*
* @default - Use service defaults
*/
readonly logGroupProps?: logs.BaseLogGroupProps;
}

export interface FunctionProps extends FunctionOptions {
Expand Down Expand Up @@ -680,6 +690,7 @@ export class Function extends FunctionBase {
public readonly _layers: ILayerVersion[] = [];

private _logGroup?: logs.ILogGroup;
private _logGroupProps?: logs.LogGroupProps;

/**
* Environment variables for this function
Expand Down Expand Up @@ -875,7 +886,18 @@ export class Function extends FunctionBase {
this.addEventSource(event);
}

// Log retention
// Can use only one log group implementation
if (props.logGroupProps && props.logRetention) {
throw new Error('Only one of "logGroupProps" or "logRetention" is allowed, but not both. Prefer to use "logGroupProps".');
}

// Log Group
this._logGroupProps = props.logGroupProps;
if (this._logGroupProps) {
this.logGroup;
}

// Log retention @deprecated
if (props.logRetention) {
const logRetention = new logs.LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
Expand Down Expand Up @@ -1100,11 +1122,24 @@ export class Function extends FunctionBase {
*/
public get logGroup(): logs.ILogGroup {
if (!this._logGroup) {
const logRetention = new logs.LogRetention(this, 'LogRetention', {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: logs.RetentionDays.INFINITE,
const managedLogGroup = new logs.ServiceManagedLogGroup(this, 'LogGroup', this._logGroupProps);
managedLogGroup.bind({
parent: this,
logGroupArn: Stack.of(this).formatArn({
service: 'logs',
resource: 'log-group',
resourceName: `/aws/lambda/${this.functionName}`,
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
}),
tagging: {
service: 'lambda',
action: 'ListTags',
requestField: 'Resource',
responseField: 'Tags',
},
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logRetention.logGroupArn);

this._logGroup = managedLogGroup;
}
return this._logGroup;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-logs/lib/data-protection-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class DataProtectionPolicy {
/**
* @internal
*/
public _bind(_scope: Construct): DataProtectionPolicyConfig {
public _bind(scope: Construct): DataProtectionPolicyConfig {
const name = this.dataProtectionPolicyProps.name || 'data-protection-policy-cdk';
const description = this.dataProtectionPolicyProps.description || 'cdk generated data protection policy';
const version = '2021-06-01';
Expand All @@ -45,7 +45,7 @@ export class DataProtectionPolicy {

const identifierArns: string[] = [];
for (let identifier of this.dataProtectionPolicyProps.identifiers) {
identifierArns.push(Stack.of(_scope).formatArn({
identifierArns.push(Stack.of(scope).formatArn({
resource: 'data-identifier',
region: '',
account: 'aws',
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk-lib/aws-logs/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './cross-account-destination';
export * from './log-group-base';
export * from './log-group';
export * from './log-stream';
export * from './metric-filter';
Expand All @@ -8,6 +9,7 @@ export * from './log-retention';
export * from './policy';
export * from './query-definition';
export * from './data-protection-policy';
export * from './service-managed-log-group';

// AWS::Logs CloudFormation Resources:
export * from './logs.generated';
171 changes: 171 additions & 0 deletions packages/aws-cdk-lib/aws-logs/lib/log-group-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@

import { ILogGroup, MetricFilterOptions, StreamOptions, SubscriptionFilterOptions } from './log-group';
import { LogStream } from './log-stream';
import { MetricFilter } from './metric-filter';
import { FilterPattern } from './pattern';
import { ResourcePolicy } from './policy';
import { SubscriptionFilter } from './subscription-filter';
import * as cloudwatch from '../../aws-cloudwatch';
import * as iam from '../../aws-iam';
import { Arn, ArnFormat, Resource } from '../../core';

/**
* A CloudWatch Log Group
*/
export abstract class LogGroupBase extends Resource implements ILogGroup {
/**
* The ARN of this log group, with ':*' appended
*/
public abstract readonly logGroupArn: string;

/**
* The name of this log group
*/
public abstract readonly logGroupName: string;

private policy?: ResourcePolicy;

/**
* Create a new Log Stream for this Log Group
*
* @param id Unique identifier for the construct in its parent
* @param props Properties for creating the LogStream
*/
public addStream(id: string, props: StreamOptions = {}): LogStream {
return new LogStream(this, id, {
logGroup: this,
...props,
});
}

/**
* Create a new Subscription Filter on this Log Group
*
* @param id Unique identifier for the construct in its parent
* @param props Properties for creating the SubscriptionFilter
*/
public addSubscriptionFilter(id: string, props: SubscriptionFilterOptions): SubscriptionFilter {
return new SubscriptionFilter(this, id, {
logGroup: this,
...props,
});
}

/**
* Create a new Metric Filter on this Log Group
*
* @param id Unique identifier for the construct in its parent
* @param props Properties for creating the MetricFilter
*/
public addMetricFilter(id: string, props: MetricFilterOptions): MetricFilter {
return new MetricFilter(this, id, {
logGroup: this,
...props,
});
}

/**
* Extract a metric from structured log events in the LogGroup
*
* Creates a MetricFilter on this LogGroup that will extract the value
* of the indicated JSON field in all records where it occurs.
*
* The metric will be available in CloudWatch Metrics under the
* indicated namespace and name.
*
* @param jsonField JSON field to extract (example: '$.myfield')
* @param metricNamespace Namespace to emit the metric under
* @param metricName Name to emit the metric under
* @returns A Metric object representing the extracted metric
*/
public extractMetric(jsonField: string, metricNamespace: string, metricName: string) {
new MetricFilter(this, `${metricNamespace}_${metricName}`, {
logGroup: this,
metricNamespace,
metricName,
filterPattern: FilterPattern.exists(jsonField),
metricValue: jsonField,
});

return new cloudwatch.Metric({ metricName, namespace: metricNamespace }).attachTo(this);
}

/**
* Give permissions to create and write to streams in this log group
*/
public grantWrite(grantee: iam.IGrantable) {
return this.grant(grantee, 'logs:CreateLogStream', 'logs:PutLogEvents');
}

/**
* Give permissions to read and filter events from this log group
*/
public grantRead(grantee: iam.IGrantable) {
return this.grant(grantee,
'logs:FilterLogEvents',
'logs:GetLogEvents',
'logs:GetLogGroupFields',
'logs:DescribeLogGroups',
'logs:DescribeLogStreams',
);
}

/**
* Give the indicated permissions on this log group and all streams
*/
public grant(grantee: iam.IGrantable, ...actions: string[]) {
return iam.Grant.addToPrincipalOrResource({
grantee,
actions,
// A LogGroup ARN out of CloudFormation already includes a ':*' at the end to include the log streams under the group.
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#w2ab1c21c10c63c43c11
resourceArns: [this.logGroupArn],
resource: this,
});
}

/**
* Public method to get the physical name of this log group
* @returns Physical name of log group
*/
public logGroupPhysicalName(): string {
return this.physicalName;
}

/**
* Adds a statement to the resource policy associated with this log group.
* A resource policy will be automatically created upon the first call to `addToResourcePolicy`.
*
* Any ARN Principals inside of the statement will be converted into AWS Account ID strings
* because CloudWatch Logs Resource Policies do not accept ARN principals.
*
* @param statement The policy statement to add
*/
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
if (!this.policy) {
this.policy = new ResourcePolicy(this, 'Policy');
}
this.policy.document.addStatements(statement.copy({
principals: statement.principals.map(p => this.convertArnPrincipalToAccountId(p)),
}));
return { statementAdded: true, policyDependable: this.policy };
}

private convertArnPrincipalToAccountId(principal: iam.IPrincipal) {
if (principal.principalAccount) {
// we use ArnPrincipal here because the constructor inserts the argument
// into the template without mutating it, which means that there is no
// ARN created by this call.
return new iam.ArnPrincipal(principal.principalAccount);
}

if (principal instanceof iam.ArnPrincipal) {
const parsedArn = Arn.split(principal.arn, ArnFormat.SLASH_RESOURCE_NAME);
if (parsedArn.account) {
return new iam.ArnPrincipal(parsedArn.account);
}
}

return principal;
}
}
Loading
Loading