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 158b290
Show file tree
Hide file tree
Showing 10 changed files with 1,125 additions and 185 deletions.
3 changes: 3 additions & 0 deletions packages/@aws-cdk-testing/framework-integ/integ.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"language": ["typescript"]
}
28 changes: 28 additions & 0 deletions packages/aws-cdk-lib/aws-lambda/lib/function-log-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Construct } from 'constructs';
import { IFunction } from './function-base';
import { BaseLogGroupProps, ServiceManagedLogGroup } from '../../aws-logs';

export interface FunctionLogGroupProps extends BaseLogGroupProps {
/**
* Name of the log group.
*/
readonly logGroupName: string;

/**
* The resource owning the log group.
*/
readonly parent: IFunction;
}

export class FunctionLogGroup extends ServiceManagedLogGroup {
constructor(scope: Construct, id: string, props: FunctionLogGroupProps) {
super(scope, id, {
...props,
logGroupName: `/aws/lambda/${props.parent.functionName}`,
tagging: {
service: 'lambda',
action: 'ListTags',
},
});
}
}
31 changes: 27 additions & 4 deletions packages/aws-cdk-lib/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IEventSource } from './event-source';
import { FileSystem } from './filesystem';
import { FunctionAttributes, FunctionBase, IFunction } from './function-base';
import { calculateFunctionHash, trimFromStart } from './function-hash';
import { FunctionLogGroup } from './function-log-group';
import { Handler } from './handler';
import { LambdaInsightsVersion } from './lambda-insights';
import { Version, VersionOptions } from './lambda-version';
Expand Down Expand Up @@ -310,6 +311,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 +320,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 +329,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 +389,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 +691,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 +887,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 +1123,11 @@ export class Function extends FunctionBase {
*/
public get logGroup(): logs.ILogGroup {
if (!this._logGroup) {
const logRetention = new logs.LogRetention(this, 'LogRetention', {
this._logGroup = new FunctionLogGroup(this, 'LogGroup', {
...this._logGroupProps,
parent: this,
logGroupName: `/aws/lambda/${this.functionName}`,
retention: logs.RetentionDays.INFINITE,
});
this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logRetention.logGroupArn);
}
return this._logGroup;
}
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';

/**
* An 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 158b290

Please sign in to comment.