Skip to content

Commit

Permalink
feat(logs): generic service managed log group to replace log retention
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Oct 6, 2023
1 parent 8bb221d commit 9350375
Show file tree
Hide file tree
Showing 11 changed files with 1,259 additions and 189 deletions.
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

0 comments on commit 9350375

Please sign in to comment.