diff --git a/packages/@aws-cdk/kinesis/lib/stream.ts b/packages/@aws-cdk/kinesis/lib/stream.ts index e3c4e542abe7a..1ae7e25f2cd20 100644 --- a/packages/@aws-cdk/kinesis/lib/stream.ts +++ b/packages/@aws-cdk/kinesis/lib/stream.ts @@ -1,6 +1,7 @@ -import { Construct, Output, PolicyStatement, Token } from '@aws-cdk/core'; -import { IIdentityResource } from '@aws-cdk/iam'; +import { Construct, FnConcat, FnSelect, FnSplit, FnSub, Output, PolicyStatement, ServicePrincipal, Stack, Token } from '@aws-cdk/core'; +import { IIdentityResource, Role } from '@aws-cdk/iam'; import * as kms from '@aws-cdk/kms'; +import { CrossAccountDestination, ISubscriptionDestination, LogGroup, SubscriptionDestination } from '@aws-cdk/logs'; import { cloudformation, StreamArn } from './kinesis.generated'; /** @@ -37,7 +38,7 @@ export interface StreamRefProps { * StreamRef.import(this, 'MyImportedStream', ref); * */ -export abstract class StreamRef extends Construct { +export abstract class StreamRef extends Construct implements ISubscriptionDestination { /** * Creates a Stream construct that represents an external stream. * @@ -55,11 +56,21 @@ export abstract class StreamRef extends Construct { */ public abstract readonly streamArn: StreamArn; + /** + * The name of the stream + */ + public abstract readonly streamName: StreamName; + /** * Optional KMS encryption key associated with this stream. */ public abstract readonly encryptionKey?: kms.EncryptionKeyRef; + /** + * The role that can be used by CloudWatch logs to write to this stream + */ + private cloudWatchLogsRole?: Role; + /** * Exports this stream from the stack. */ @@ -159,6 +170,45 @@ export abstract class StreamRef extends Construct { ); } + public subscriptionDestination(sourceLogGroup: LogGroup): SubscriptionDestination { + // Following example from https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#DestinationKinesisExample + if (!this.cloudWatchLogsRole) { + // Create a role to be assumed by CWL that can write to this stream and pass itself. + this.cloudWatchLogsRole = new Role(this, 'CloudWatchLogsCanPutRecords', { + assumedBy: new ServicePrincipal(new FnSub('logs.${AWS::Region}.amazonaws.com')), + }); + this.cloudWatchLogsRole.addToPolicy(new PolicyStatement().addAction('kinesis:PutRecord').addResource(this.streamArn)); + this.cloudWatchLogsRole.addToPolicy(new PolicyStatement().addAction('iam:PassRole').addResource(this.cloudWatchLogsRole.roleArn)); + } + + // We've now made it possible for CloudWatch events to write to us. In case the LogGroup is in a + // different account, we must add a Destination in between as well. + const sourceStack = Stack.find(sourceLogGroup); + const thisStack = Stack.find(this); + + // Case considered: if both accounts are undefined, we can't make any assumptions. Better + // to assume we don't need to do anything special. + const sameAccount = sourceStack.env.account === thisStack.env.account; + + if (sameAccount) { + return { arn: this.streamArn, role: this.cloudWatchLogsRole }; + } + + // The destination lives in the target account + const dest = new CrossAccountDestination(this, 'CloudWatchCrossAccountDestination', { + // Unfortunately destinationName is required so we have to invent one that won't conflict. + destinationName: new FnConcat(sourceLogGroup.logGroupName, 'To', this.streamName) as any, + targetArn: this.streamArn, + role: this.cloudWatchLogsRole + }); + dest.addToPolicy(new PolicyStatement() + .addAction('logs:PutSubscriptionFilter') + .addAwsAccountPrincipal(sourceStack.env.account) + .addAllResources()); + + return dest.subscriptionDestination(sourceLogGroup); + } + private grant(identity: IIdentityResource, actions: { streamActions: string[], keyActions: string[] }) { identity.addToPolicy(new PolicyStatement() .addResource(this.streamArn) @@ -307,12 +357,17 @@ export class StreamName extends Token {} class ImportedStreamRef extends StreamRef { public readonly streamArn: StreamArn; + public readonly streamName: StreamName; public readonly encryptionKey?: kms.EncryptionKeyRef; constructor(parent: Construct, name: string, props: StreamRefProps) { super(parent, name); this.streamArn = props.streamArn; + // ARN always looks like: arn:aws:kinesis:us-east-2:123456789012:stream/mystream + // so we can get the name from the ARN. + this.streamName = new FnSelect(1, new FnSplit('/', this.streamArn)); + if (props.encryptionKey) { this.encryptionKey = kms.EncryptionKeyRef.import(parent, 'Key', props.encryptionKey); } else { diff --git a/packages/@aws-cdk/kinesis/package.json b/packages/@aws-cdk/kinesis/package.json index 6a3a57f3745eb..ad5620e03b1e9 100644 --- a/packages/@aws-cdk/kinesis/package.json +++ b/packages/@aws-cdk/kinesis/package.json @@ -41,6 +41,7 @@ "dependencies": { "@aws-cdk/core": "^0.7.3-beta", "@aws-cdk/iam": "^0.7.3-beta", - "@aws-cdk/kms": "^0.7.3-beta" + "@aws-cdk/kms": "^0.7.3-beta", + "@aws-cdk/logs": "^0.7.3-beta" } } diff --git a/packages/@aws-cdk/kinesis/test/test.subscriptiondestination.ts b/packages/@aws-cdk/kinesis/test/test.subscriptiondestination.ts new file mode 100644 index 0000000000000..4aeb8721fd45b --- /dev/null +++ b/packages/@aws-cdk/kinesis/test/test.subscriptiondestination.ts @@ -0,0 +1,80 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { FilterPattern, LogGroup, SubscriptionFilter } from '@aws-cdk/logs'; +import { Test } from 'nodeunit'; +import { Stream } from '../lib'; + +export = { + 'stream can be subscription destination'(test: Test) { + // GIVEN + const stack = new Stack(); + const stream = new Stream(stack, 'MyStream'); + const logGroup = new LogGroup(stack, 'LogGroup'); + + // WHEN + new SubscriptionFilter(stack, 'Subscription', { + logGroup, + destination: stream, + filterPattern: FilterPattern.allEvents() + }); + + // THEN: subscription target is Stream + expect(stack).to(haveResource('AWS::Logs::SubscriptionFilter', { + DestinationArn: { "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] }, + RoleArn: { "Fn::GetAtt": [ "MyStreamCloudWatchLogsCanPutRecords58498490", "Arn" ] }, + })); + + // THEN: we have a role to write to the Lambda + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: "sts:AssumeRole", + Principal: { Service: { "Fn::Sub": "logs.${AWS::Region}.amazonaws.com" }} + }], + } + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "kinesis:PutRecord", + Effect: "Allow", + Resource: { "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] } + }, + { + Action: "iam:PassRole", + Effect: "Allow", + Resource: { "Fn::GetAtt": [ "MyStreamCloudWatchLogsCanPutRecords58498490", "Arn" ] } + } + ], + } + })); + + test.done(); + }, + + 'cross-account stream can be subscription destination with Destination'(test: Test) { + // GIVEN + const sourceStack = new Stack(undefined, undefined, { env: { account: '12345' }}); + const logGroup = new LogGroup(sourceStack, 'LogGroup'); + + const destStack = new Stack(undefined, undefined, { env: { account: '67890' }}); + const stream = new Stream(destStack, 'MyStream'); + + // WHEN + new SubscriptionFilter(sourceStack, 'Subscription', { + logGroup, + destination: stream, + filterPattern: FilterPattern.allEvents() + }); + + // THEN: the source stack has a Destination object that the subscription points to + expect(destStack).to(haveResource('AWS::Logs::Destination', { + TargetArn: { "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] }, + RoleArn: { "Fn::GetAtt": [ "MyStreamCloudWatchLogsCanPutRecords58498490", "Arn" ] }, + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/lambda/lib/lambda-ref.ts b/packages/@aws-cdk/lambda/lib/lambda-ref.ts index c5c1616a5433c..f8a1ba2bb8bf1 100644 --- a/packages/@aws-cdk/lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/lambda/lib/lambda-ref.ts @@ -3,7 +3,7 @@ import { AccountPrincipal, Arn, Construct, FnSelect, FnSplit, FnSub, PolicyPrincipal, PolicyStatement, resolve, ServicePrincipal, Token } from '@aws-cdk/core'; import { EventRuleTarget, IEventRuleTarget } from '@aws-cdk/events'; import { Role } from '@aws-cdk/iam'; -import { ISubscriptionDestination, SubscriptionDestinationProps } from '@aws-cdk/logs'; +import { ISubscriptionDestination, LogGroup, LogGroupArn, SubscriptionDestination } from '@aws-cdk/logs'; import { cloudformation, FunctionArn } from './lambda.generated'; import { LambdaPermission } from './permission'; @@ -117,7 +117,7 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget, I /** * Indicates if the policy that allows CloudWatch logs to publish to this topic has been added. */ - private logSubscriptionDestinationPolicyAdded = false; + private logSubscriptionDestinationPolicyAddedFor: LogGroupArn[] = []; /** * Adds a permission to the Lambda resource policy. @@ -218,20 +218,21 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget, I return this.metric('Throttles', { statistic: 'sum', ...props }); } - public get subscriptionDestinationProps(): SubscriptionDestinationProps { - if (!this.logSubscriptionDestinationPolicyAdded) { - // FIXME: this limits to the same region, which shouldn't really be an issue. - // Wildcards in principals are unfortunately not supported. + public subscriptionDestination(sourceLogGroup: LogGroup): SubscriptionDestination { + const arn = sourceLogGroup.logGroupArn; + + if (this.logSubscriptionDestinationPolicyAddedFor.indexOf(arn) === -1) { + // NOTE: the use of {AWS::Region} limits this to the same region, which shouldn't really be an issue, + // since the Lambda must be in the same region as the SubscriptionFilter anyway. // - // Whitelisting the whole of CWL is not as secure as the example in - // https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample - // (which also limits on source ARN) but this is far simpler and we trust CloudWatch Logs. + // (Wildcards in principals are unfortunately not supported. this.addPermission('InvokedByCloudWatchLogs', { - principal: new ServicePrincipal(new FnSub('logs.${AWS::Region}.amazonaws.com')) + principal: new ServicePrincipal(new FnSub('logs.${AWS::Region}.amazonaws.com')), + sourceArn: arn }); - this.logSubscriptionDestinationPolicyAdded = true; + this.logSubscriptionDestinationPolicyAddedFor.push(arn); } - return new SubscriptionDestinationProps(this.functionArn); + return { arn: this.functionArn }; } private parsePermissionPrincipal(principal?: PolicyPrincipal) { diff --git a/packages/@aws-cdk/lambda/test/test.subscriptiondestination.ts b/packages/@aws-cdk/lambda/test/test.subscriptiondestination.ts index 8a285bde8bfcc..ea59c99eed455 100644 --- a/packages/@aws-cdk/lambda/test/test.subscriptiondestination.ts +++ b/packages/@aws-cdk/lambda/test/test.subscriptiondestination.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; -import { LogGroup, LogPattern, SubscriptionFilter } from '@aws-cdk/logs'; +import { FilterPattern, LogGroup, SubscriptionFilter } from '@aws-cdk/logs'; import { Test } from 'nodeunit'; import { Lambda, LambdaInlineCode, LambdaRuntime } from '../lib'; @@ -19,7 +19,7 @@ export = { new SubscriptionFilter(stack, 'Subscription', { logGroup, destination: lambda, - logPattern: LogPattern.allEvents() + filterPattern: FilterPattern.allEvents() }); // THEN: subscription target is Lambda diff --git a/packages/@aws-cdk/logs/README.md b/packages/@aws-cdk/logs/README.md index 8b84a61966376..585486e1ebabe 100644 --- a/packages/@aws-cdk/logs/README.md +++ b/packages/@aws-cdk/logs/README.md @@ -1,6 +1,6 @@ ## AWS CloudWatch Logs Construct Library -This library supplies Constructs for working with CloudWatch Logs. +This library supplies constructs for working with CloudWatch Logs. ### Log Groups/Streams @@ -43,7 +43,7 @@ const logGroup = new LogGroup(this, 'LogGroup', { ... }); new SubscriptionFilter(this, 'Subscription', { logGroup, destination: lambda, - logPattern: LogPattern.allTerms("ERROR", "MainThread") + filterPattern: FilterPattern.allTerms("ERROR", "MainThread") }); ``` @@ -73,13 +73,13 @@ are three types of patterns: * JSON patterns * Space-delimited table patterns -All patterns are constructed by using static functions on the `LogPattern` +All patterns are constructed by using static functions on the `FilterPattern` class. In addition to the patterns above, the following special patterns exist: -* `LogPattern.allEvents()`: matches all log events. -* `LogPattern.literal(string)`: if you already know what pattern expression to +* `FilterPattern.allEvents()`: matches all log events. +* `FilterPattern.literal(string)`: if you already know what pattern expression to use, this function takes a string and will use that as the log pattern. For more information, see the [Filter and Pattern Syntax](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html). @@ -89,9 +89,11 @@ In addition to the patterns above, the following special patterns exist: Text patterns match if the literal strings appear in the text form of the log line. -* `LogPattern.allTerms(term, term, ...)`: matches if all of the given terms +* `FilterPattern.allTerms(term, term, ...)`: matches if all of the given terms (substrings) appear in the log event. -* `LogPattern.anyGroup([term, term, ...], [term, term, ...], ...)`: matches if +* `FilterPattern.anyTerm(term, term, ...)`: matches if all of the given terms + (substrings) appear in the log event. +* `FilterPattern.anyGroup([term, term, ...], [term, term, ...], ...)`: matches if all of the terms in any of the groups (specified as arrays) matches. This is an OR match. @@ -99,9 +101,12 @@ line. Examples: ```ts -const pattern1 = LogPattern.allTerms('ERROR', 'MainThread'); +// Search for lines that contain both "ERROR" and "MainThread" +const pattern1 = FilterPattern.allTerms('ERROR', 'MainThread'); -const pattern2 = LogPattern.anyGroup( +// Search for lines that either contain both "ERROR" and "MainThread", or +// both "WARN" and "Deadlock". +const pattern2 = FilterPattern.anyGroup( ['ERROR', 'MainThread'], ['WARN', 'Deadlock'], ); @@ -122,22 +127,22 @@ fields. Fields in the JSON structure are identified by identifier the complete object as `$` and then descending into it, such as `$.field` or `$.list[0].field`. -* `LogPattern.stringValue(field, comparison, string)`: matches if the given +* `FilterPattern.stringValue(field, comparison, string)`: matches if the given field compares as indicated with the given string value. -* `LogPattern.numberValue(field, comparison, number)`: matches if the given +* `FilterPattern.numberValue(field, comparison, number)`: matches if the given field compares as indicated with the given numerical value. -* `LogPattern.isNull(field)`: matches if the given field exists and has the +* `FilterPattern.isNull(field)`: matches if the given field exists and has the value `null`. -* `LogPattern.notExists(field)`: matches if the given field is not in the JSON +* `FilterPattern.notExists(field)`: matches if the given field is not in the JSON structure. -* `LogPattern.exists(field)`: matches if the given field is in the JSON +* `FilterPattern.exists(field)`: matches if the given field is in the JSON structure. -* `LogPattern.booleanValue(field, boolean)`: matches if the given field +* `FilterPattern.booleanValue(field, boolean)`: matches if the given field is exactly the given boolean value. -* `LogPattern.all(jsonPattern, jsonPattern, ...)`: matches if all of the +* `FilterPattern.all(jsonPattern, jsonPattern, ...)`: matches if all of the given JSON patterns match. This makes an AND combination of the given patterns. -* `LogPattern.any(jsonPattern, jsonPattern, ...)`: matches if any of the +* `FilterPattern.any(jsonPattern, jsonPattern, ...)`: matches if any of the given JSON patterns match. This makes an OR combination of the given patterns. @@ -145,11 +150,14 @@ and then descending into it, such as `$.field` or `$.list[0].field`. Example: ```ts -const pattern = LogPattern.all( - LogPattern.stringValue('$.component', '=', 'HttpServer'), - LogPattern.any( - LogPattern.booleanValue('$.error', true), - LogPattern.numberValue('$.latency', '>', 1000) +// Search for all events where the component field is equal to +// "HttpServer" and either error is true or the latency is higher +// than 1000. +const pattern = FilterPattern.all( + FilterPattern.stringValue('$.component', '=', 'HttpServer'), + FilterPattern.any( + FilterPattern.booleanValue('$.error', true), + FilterPattern.numberValue('$.latency', '>', 1000) )); ``` @@ -163,7 +171,7 @@ logs. Text that is surrounded by `"..."` quotes or `[...]` square brackets will be treated as one column. -* `LogPattern.spaceDelimited(column, column, ...)`: construct a +* `FilterPattern.spaceDelimited(column, column, ...)`: construct a `SpaceDelimitedTextPattern` object with the indicated columns. The columns map one-by-one the columns found in the log event. The string `"..."` may be used to specify an arbitrary number of unnamed columns anywhere in the @@ -182,7 +190,9 @@ Multiple restrictions can be added on the same column; they must all apply. Example: ```ts -const pattern = LogPattern.spaceDelimited('time', 'component', '...', 'result_code', 'latency') +// Search for all events where the component is "HttpServer" and the +// result code is not equal to 200. +const pattern = FilterPattern.spaceDelimited('time', 'component', '...', 'result_code', 'latency') .whereString('component', '=', 'HttpServer') .whereNumber('result_code', '!=', 200); ``` diff --git a/packages/@aws-cdk/logs/lib/destination.ts b/packages/@aws-cdk/logs/lib/cross-account-destination.ts similarity index 59% rename from packages/@aws-cdk/logs/lib/destination.ts rename to packages/@aws-cdk/logs/lib/cross-account-destination.ts index bbe315b67ee66..5be7f521926b1 100644 --- a/packages/@aws-cdk/logs/lib/destination.ts +++ b/packages/@aws-cdk/logs/lib/cross-account-destination.ts @@ -1,17 +1,8 @@ import { Arn, Construct, PolicyDocument, PolicyStatement, Token } from '@aws-cdk/core'; import { Role } from '@aws-cdk/iam'; +import { LogGroup } from './log-group'; import { cloudformation, DestinationArn } from './logs.generated'; -import { ISubscriptionDestination, SubscriptionDestinationProps } from './subscriptionfilter'; - -/** - * Interface for classes that can be the target of a Log Destination - */ -export interface ILogDestinationTarget { - /** - * Return the ARN of the log destination target - */ - readonly destinationTargetArn: Arn; -} +import { ISubscriptionDestination, SubscriptionDestination } from './subscription-filter'; export interface DestinationProps { /** @@ -27,26 +18,28 @@ export interface DestinationProps { role: Role; /** - * The log destination target + * The log destination target's ARN */ - target: ILogDestinationTarget; + targetArn: Arn; } /** * Create a new CloudWatch Logs Destination. * - * Log destinations can be used to subscribe a Kinesis stream - * in a different account to a CloudWatch Subscription. + * Log destinations can be used to subscribe a Kinesis stream in a different + * account to a CloudWatch Subscription. A Kinesis stream in the same account + * can be subscribed directly. * - * A Kinesis stream in the same account can be subscribed directly. + * The @aws-cdk/kinesis library takes care of this automatically; you shouldn't + * need to bother with this class. */ -export class Destination extends Construct implements ISubscriptionDestination { +export class CrossAccountDestination extends Construct implements ISubscriptionDestination { public readonly policyDocument: PolicyDocument = new PolicyDocument(); public readonly destinationName: DestinationName; public readonly destinationArn: DestinationArn; - constructor(parent: Construct, name: string, props: DestinationProps) { - super(parent, name); + constructor(parent: Construct, id: string, props: DestinationProps) { + super(parent, id); this.policyDocument = new PolicyDocument(); @@ -54,7 +47,7 @@ export class Destination extends Construct implements ISubscriptionDestination { destinationName: props.destinationName, destinationPolicy: new Token(() => !this.policyDocument.isEmpty ? JSON.stringify(this.policyDocument.resolve()) : ""), roleArn: props.role.roleArn, - targetArn: props.target.destinationTargetArn + targetArn: props.targetArn }); this.destinationArn = resource.destinationArn; @@ -65,8 +58,8 @@ export class Destination extends Construct implements ISubscriptionDestination { this.policyDocument.addStatement(statement); } - public get subscriptionDestinationProps(): SubscriptionDestinationProps { - return new SubscriptionDestinationProps(this.destinationArn); + public subscriptionDestination(_sourceLogGroup: LogGroup): SubscriptionDestination { + return { arn: this.destinationArn }; } } diff --git a/packages/@aws-cdk/logs/lib/index.ts b/packages/@aws-cdk/logs/lib/index.ts index 10b09551c951c..bf626238843c3 100644 --- a/packages/@aws-cdk/logs/lib/index.ts +++ b/packages/@aws-cdk/logs/lib/index.ts @@ -1,9 +1,9 @@ +export * from './cross-account-destination'; +export * from './log-group'; +export * from './log-stream'; +export * from './metric-filter'; +export * from './pattern'; +export * from './subscription-filter'; + // AWS::Logs CloudFormation Resources: export * from './logs.generated'; - -export * from './destination'; -export * from './loggroup'; -export * from './logstream'; -export * from './metricfilter'; -export * from './pattern'; -export * from './subscriptionfilter'; diff --git a/packages/@aws-cdk/logs/lib/log-group.ts b/packages/@aws-cdk/logs/lib/log-group.ts new file mode 100644 index 0000000000000..69ed473c9a94b --- /dev/null +++ b/packages/@aws-cdk/logs/lib/log-group.ts @@ -0,0 +1,185 @@ +import { Construct, Token } from '@aws-cdk/core'; +import { LogStream } from './log-stream'; +import { cloudformation, LogGroupArn } from './logs.generated'; +import { MetricFilter } from './metric-filter'; +import { IFilterPattern } from './pattern'; +import { ISubscriptionDestination, SubscriptionFilter } from './subscription-filter'; + +/** + * Properties for a new LogGroup + */ +export interface LogGroupProps { + /** + * Name of the log group. + * + * @default Automatically generated + */ + logGroupName?: string; + + /** + * How long, in days, the log contents will be retained. + * + * To retain all logs, set this value to Infinity. + * + * @default 730 days (2 years) + */ + retentionDays?: number; +} + +/** + * Create a new CloudWatch Log Group + */ +export class LogGroup extends Construct { + /** + * The ARN of this log group + */ + public readonly logGroupArn: LogGroupArn; + + /** + * The name of this log group + */ + public readonly logGroupName: LogGroupName; + + constructor(parent: Construct, id: string, props: LogGroupProps = {}) { + super(parent, id); + + let retentionInDays = props.retentionDays; + if (retentionInDays === undefined) { retentionInDays = 730; } + if (retentionInDays === Infinity) { retentionInDays = undefined; } + + if (retentionInDays !== undefined && retentionInDays <= 0) { + throw new Error(`retentionInDays must be positive, got ${retentionInDays}`); + } + + const resource = new cloudformation.LogGroupResource(this, 'Resource', { + logGroupName: props.logGroupName, + retentionInDays, + }); + + this.logGroupArn = resource.logGroupArn; + this.logGroupName = resource.ref; + } + + /** + * Create a new Log Stream for this Log Group + * + * @param parent Parent construct + * @param id Unique identifier for the construct in its parent + * @param props Properties for creating the LogStream + */ + public newStream(parent: Construct, id: string, props: NewLogStreamProps = {}): LogStream { + return new LogStream(parent, id, { + logGroup: this, + ...props + }); + } + + /** + * Create a new Subscription Filter on this Log Group + * + * @param parent Parent construct + * @param id Unique identifier for the construct in its parent + * @param props Properties for creating the SubscriptionFilter + */ + public newSubscriptionFilter(parent: Construct, id: string, props: NewSubscriptionFilterProps): SubscriptionFilter { + return new SubscriptionFilter(parent, id, { + logGroup: this, + ...props + }); + } + + /** + * Create a new Metric Filter on this Log Group + * + * @param parent Parent construct + * @param id Unique identifier for the construct in its parent + * @param props Properties for creating the MetricFilter + */ + public newMetricFilter(parent: Construct, id: string, props: NewMetricFilterProps): MetricFilter { + return new MetricFilter(parent, id, { + logGroup: this, + ...props + }); + } +} + +/** + * Name of a log group + */ +export class LogGroupName extends Token { +} + +/** + * Properties for a new LogStream created from a LogGroup + */ +export interface NewLogStreamProps { + /** + * The name of the log stream to create. + * + * The name must be unique within the log group. + * + * @default Automatically generated + */ + logStreamName?: string; +} + +/** + * Properties for a new SubscriptionFilter created from a LogGroup + */ +export interface NewSubscriptionFilterProps { + /** + * The destination to send the filtered events to. + * + * For example, a Kinesis stream or a Lambda function. + */ + destination: ISubscriptionDestination; + + /** + * Log events matching this pattern will be sent to the destination. + */ + filterPattern: IFilterPattern; +} + +/** + * Properties for a MetricFilter created from a LogGroup + */ +export interface NewMetricFilterProps { + /** + * Pattern to search for log events. + */ + filterPattern: IFilterPattern; + + /** + * The namespace of the metric to emit. + */ + metricNamespace: string; + + /** + * The name of the metric to emit. + */ + metricName: string; + + /** + * The value to emit for the metric. + * + * Can either be a literal number (typically "1"), or the name of a field in the structure + * to take the value from the matched event. If you are using a field value, the field + * value must have been matched using the pattern. + * + * If you want to specify a field from a matched JSON structure, use '$.fieldName', + * and make sure the field is in the pattern (if only as '$.fieldName = *'). + * + * If you want to specify a field from a matched space-delimited structure, + * use '$fieldName'. + * + * @default "1" + */ + metricValue?: string; + + /** + * The value to emit if the pattern does not match a particular event. + * + * @default No metric emitted. + */ + defaultValue?: number; +} \ No newline at end of file diff --git a/packages/@aws-cdk/logs/lib/logstream.ts b/packages/@aws-cdk/logs/lib/log-stream.ts similarity index 79% rename from packages/@aws-cdk/logs/lib/logstream.ts rename to packages/@aws-cdk/logs/lib/log-stream.ts index fc469213a02b2..1ce45f7d24fa2 100644 --- a/packages/@aws-cdk/logs/lib/logstream.ts +++ b/packages/@aws-cdk/logs/lib/log-stream.ts @@ -1,5 +1,5 @@ import { Construct, Token } from '@aws-cdk/core'; -import { LogGroup } from './loggroup'; +import { LogGroup } from './log-group'; import { cloudformation } from './logs.generated'; /** @@ -22,10 +22,13 @@ export interface LogStreamProps { } export class LogStream extends Construct { + /** + * The name of this log stream + */ public readonly logStreamName: LogStreamName; - constructor(parent: Construct, name: string, props: LogStreamProps) { - super(parent, name); + constructor(parent: Construct, id: string, props: LogStreamProps) { + super(parent, id); const resource = new cloudformation.LogStreamResource(this, 'Resource', { logGroupName: props.logGroup.logGroupName, @@ -36,5 +39,8 @@ export class LogStream extends Construct { } } +/** + * The name of a log stream + */ export class LogStreamName extends Token { } diff --git a/packages/@aws-cdk/logs/lib/loggroup.ts b/packages/@aws-cdk/logs/lib/loggroup.ts deleted file mode 100644 index bc9e4c8c63771..0000000000000 --- a/packages/@aws-cdk/logs/lib/loggroup.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Construct, Token } from '@aws-cdk/core'; -import { cloudformation, LogGroupArn } from './logs.generated'; - -/** - * Properties for a new LogGroup - */ -export interface LogGroupProps { - /** - * Name of the log group. - * - * @default Automatically generated - */ - logGroupName?: string; - - /** - * How long, in days, the log contents will be retained. - * - * To retain all logs, set this value to Infinity. - * - * @default 730 days (2 years) - */ - retentionInDays?: number; -} - -/** - * Create a new CloudWatch Log Group - */ -export class LogGroup extends Construct { - public readonly logGroupArn: LogGroupArn; - public readonly logGroupName: LogGroupName; - - constructor(parent: Construct, name: string, props?: LogGroupProps) { - super(parent, name); - - let retentionInDays = props && props.retentionInDays; - if (retentionInDays === undefined) { retentionInDays = 365; } - if (retentionInDays === Infinity) { retentionInDays = undefined; } - - if (retentionInDays !== undefined && retentionInDays <= 0) { - throw new Error(`retentionInDays must be positive, got ${retentionInDays}`); - } - - const resource = new cloudformation.LogGroupResource(this, 'Resource', { - logGroupName: props && props.logGroupName, - retentionInDays, - }); - - this.logGroupArn = resource.logGroupArn; - this.logGroupName = resource.ref; - } -} - -/** - * Name of a log group - */ -export class LogGroupName extends Token { -} diff --git a/packages/@aws-cdk/logs/lib/metricfilter.ts b/packages/@aws-cdk/logs/lib/metric-filter.ts similarity index 86% rename from packages/@aws-cdk/logs/lib/metricfilter.ts rename to packages/@aws-cdk/logs/lib/metric-filter.ts index d9ec00705f5ab..3c9e54e63554e 100644 --- a/packages/@aws-cdk/logs/lib/metricfilter.ts +++ b/packages/@aws-cdk/logs/lib/metric-filter.ts @@ -1,7 +1,7 @@ import { Construct } from '@aws-cdk/core'; -import { LogGroup } from './loggroup'; +import { LogGroup } from './log-group'; import { cloudformation } from './logs.generated'; -import { ILogPattern } from './pattern'; +import { IFilterPattern } from './pattern'; /** * Properties for a MetricFilter @@ -15,7 +15,7 @@ export interface MetricFilterProps { /** * Pattern to search for log events. */ - logPattern: ILogPattern; + filterPattern: IFilterPattern; /** * The namespace of the metric to emit. @@ -56,12 +56,12 @@ export interface MetricFilterProps { * A filter that extracts information from CloudWatch Logs and emits to CloudWatch Metrics */ export class MetricFilter extends Construct { - constructor(parent: Construct, name: string, props: MetricFilterProps) { - super(parent, name); + constructor(parent: Construct, id: string, props: MetricFilterProps) { + super(parent, id); new cloudformation.MetricFilterResource(this, 'Resource', { logGroupName: props.logGroup.logGroupName, - filterPattern: props.logPattern.logPatternString, + filterPattern: props.filterPattern.logPatternString, metricTransformations: [{ metricNamespace: props.metricNamespace, metricName: props.metricName, diff --git a/packages/@aws-cdk/logs/lib/pattern.ts b/packages/@aws-cdk/logs/lib/pattern.ts index f5b6cdab00016..90a79572a5c96 100644 --- a/packages/@aws-cdk/logs/lib/pattern.ts +++ b/packages/@aws-cdk/logs/lib/pattern.ts @@ -3,14 +3,14 @@ /** * Interface for objects that can render themselves to log patterns. */ -export interface ILogPattern { +export interface IFilterPattern { readonly logPatternString: string; } /** * Base class for patterns that only match JSON log events. */ -export abstract class JSONPattern implements ILogPattern { +export abstract class JSONPattern implements IFilterPattern { // This is a separate class so we have some type safety where users can't // combine text patterns and JSON patterns with an 'and' operation. constructor(public readonly jsonPatternString: string) { @@ -24,7 +24,7 @@ export abstract class JSONPattern implements ILogPattern { /** * A collection of static methods to generate appropriate ILogPatterns */ -export class LogPattern { +export class FilterPattern { /** * Use the given string as log pattern. @@ -34,14 +34,14 @@ export class LogPattern { * * @param logPatternString The pattern string to use. */ - public static literal(logPatternString: string): ILogPattern { + public static literal(logPatternString: string): IFilterPattern { return new LiteralLogPattern(logPatternString); } /** * A log pattern that matches all events. */ - public static allEvents(): ILogPattern { + public static allEvents(): IFilterPattern { return new LiteralLogPattern(""); } @@ -50,10 +50,19 @@ export class LogPattern { * * @param terms The words to search for. All terms must match. */ - public static allTerms(...terms: string[]): ILogPattern { + public static allTerms(...terms: string[]): IFilterPattern { return new TextLogPattern([terms]); } + /** + * A log pattern that matches if any of the strings given appear in the event. + * + * @param terms The words to search for. Any terms must match. + */ + public static anyTerm(...terms: string[]): IFilterPattern { + return new TextLogPattern(terms.map(t => [t])); + } + /** * A log pattern that matches if any of the given term groups matches the event. * @@ -61,7 +70,7 @@ export class LogPattern { * * @param termGroups A list of term groups to search for. Any one of the clauses must match. */ - public static anyGroup(...termGroups: string[][]): ILogPattern { + public static anyTermGroup(...termGroups: string[][]): IFilterPattern { return new TextLogPattern(termGroups); } @@ -187,7 +196,7 @@ export class LogPattern { /** * Use a string literal as a log pattern */ -class LiteralLogPattern implements ILogPattern { +class LiteralLogPattern implements IFilterPattern { constructor(public readonly logPatternString: string) { } } @@ -195,7 +204,7 @@ class LiteralLogPattern implements ILogPattern { /** * Search for a set of set of terms */ -class TextLogPattern implements ILogPattern { +class TextLogPattern implements IFilterPattern { public readonly logPatternString: string; constructor(clauses: string[][]) { @@ -260,7 +269,7 @@ const COL_ELLIPSIS = '...'; /** * Space delimited text pattern */ -export class SpaceDelimitedTextPattern implements ILogPattern { +export class SpaceDelimitedTextPattern implements IFilterPattern { /** * Construct a new instance of a space delimited text pattern * diff --git a/packages/@aws-cdk/logs/lib/subscriptionfilter.ts b/packages/@aws-cdk/logs/lib/subscription-filter.ts similarity index 66% rename from packages/@aws-cdk/logs/lib/subscriptionfilter.ts rename to packages/@aws-cdk/logs/lib/subscription-filter.ts index 09371c28e40fc..93260b032ad34 100644 --- a/packages/@aws-cdk/logs/lib/subscriptionfilter.ts +++ b/packages/@aws-cdk/logs/lib/subscription-filter.ts @@ -1,8 +1,8 @@ import { Arn, Construct } from '@aws-cdk/core'; import { Role } from '@aws-cdk/iam'; -import { LogGroup } from './loggroup'; +import { LogGroup } from './log-group'; import { cloudformation } from './logs.generated'; -import { ILogPattern } from './pattern'; +import { IFilterPattern } from './pattern'; /** * Interface for classes that can be the destination of a log Subscription @@ -14,16 +14,28 @@ export interface ISubscriptionDestination { * If necessary, the destination can use the properties of the SubscriptionFilter * object itself to configure its permissions to allow the subscription to write * to it. + * + * The destination may reconfigure its own permissions in response to this + * function call. */ - readonly subscriptionDestinationProps: SubscriptionDestinationProps; + subscriptionDestination(sourceLogGroup: LogGroup): SubscriptionDestination; } /** * Properties returned by a Subscription destination */ -export class SubscriptionDestinationProps { - public constructor(public readonly arn: Arn, public readonly role?: Role) { - } +export interface SubscriptionDestination { + /** + * The ARN of the subscription's destination + */ + readonly arn: Arn; + + /** + * The role to assume to write log events to the destination + * + * @default No role assumed + */ + readonly role?: Role; } /** @@ -45,23 +57,23 @@ export interface SubscriptionFilterProps { /** * Log events matching this pattern will be sent to the destination. */ - logPattern: ILogPattern; + filterPattern: IFilterPattern; } /** * A new Subscription on a CloudWatch log group. */ export class SubscriptionFilter extends Construct { - constructor(parent: Construct, name: string, props: SubscriptionFilterProps) { - super(parent, name); + constructor(parent: Construct, id: string, props: SubscriptionFilterProps) { + super(parent, id); - const destProps = props.destination.subscriptionDestinationProps; + const destProps = props.destination.subscriptionDestination(props.logGroup); new cloudformation.SubscriptionFilterResource(this, 'Resource', { logGroupName: props.logGroup.logGroupName, destinationArn: destProps.arn, roleArn: destProps.role && destProps.role.roleArn, - filterPattern: props.logPattern.logPatternString + filterPattern: props.filterPattern.logPatternString }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/logs/test/example.retention.lit.ts b/packages/@aws-cdk/logs/test/example.retention.lit.ts index 65e952a5ee515..d710e8bc5e906 100644 --- a/packages/@aws-cdk/logs/test/example.retention.lit.ts +++ b/packages/@aws-cdk/logs/test/example.retention.lit.ts @@ -7,7 +7,7 @@ function shortLogGroup() { /// !show // Configure log group for short retention const logGroup = new LogGroup(stack, 'LogGroup', { - retentionInDays: 7 + retentionDays: 7 }); /// !hide return logGroup; @@ -17,7 +17,7 @@ function infiniteLogGroup() { /// !show // Configure log group for infinite retention const logGroup = new LogGroup(stack, 'LogGroup', { - retentionInDays: Infinity + retentionDays: Infinity }); /// !hide return logGroup; diff --git a/packages/@aws-cdk/logs/test/integ.metricfilter.lit.expected.json b/packages/@aws-cdk/logs/test/integ.metricfilter.lit.expected.json index 9eb21c4607f55..d6da6b5494495 100644 --- a/packages/@aws-cdk/logs/test/integ.metricfilter.lit.expected.json +++ b/packages/@aws-cdk/logs/test/integ.metricfilter.lit.expected.json @@ -3,7 +3,7 @@ "LogGroupF5B46931": { "Type": "AWS::Logs::LogGroup", "Properties": { - "RetentionInDays": 365 + "RetentionInDays": 730 } }, "MetricFilter1B93B6E5": { @@ -23,4 +23,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/logs/test/integ.metricfilter.lit.ts b/packages/@aws-cdk/logs/test/integ.metricfilter.lit.ts index c40e9d264d9a5..3c0f1227698e1 100644 --- a/packages/@aws-cdk/logs/test/integ.metricfilter.lit.ts +++ b/packages/@aws-cdk/logs/test/integ.metricfilter.lit.ts @@ -1,5 +1,5 @@ import { App, Stack, StackProps } from '@aws-cdk/core'; -import { LogGroup, LogPattern, MetricFilter } from '../lib'; +import { FilterPattern, LogGroup, MetricFilter } from '../lib'; class MetricFilterIntegStack extends Stack { constructor(parent: App, name: string, props?: StackProps) { @@ -12,7 +12,7 @@ class MetricFilterIntegStack extends Stack { logGroup, metricNamespace: 'MyApp', metricName: 'Latency', - logPattern: LogPattern.exists('$.latency'), + filterPattern: FilterPattern.exists('$.latency'), metricValue: '$.latency' }); /// !hide diff --git a/packages/@aws-cdk/logs/test/test.destination.ts b/packages/@aws-cdk/logs/test/test.destination.ts index 40373f6177128..5a7dc6648efa1 100644 --- a/packages/@aws-cdk/logs/test/test.destination.ts +++ b/packages/@aws-cdk/logs/test/test.destination.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Arn, PolicyStatement, ServicePrincipal, Stack } from '@aws-cdk/core'; import { Role } from '@aws-cdk/iam'; import { Test } from 'nodeunit'; -import { Destination, ILogDestinationTarget } from '../lib'; +import { CrossAccountDestination } from '../lib'; export = { 'simple destination'(test: Test) { @@ -13,10 +13,10 @@ export = { }); // WHEN - new Destination(stack, 'Dest', { + new CrossAccountDestination(stack, 'Dest', { destinationName: 'MyDestination', role, - target: new BogusTarget() + targetArn: new Arn('arn:bogus') }); // THEN @@ -36,10 +36,10 @@ export = { assumedBy: new ServicePrincipal('logs.us-east-2.amazonaws.com') }); - const dest = new Destination(stack, 'Dest', { + const dest = new CrossAccountDestination(stack, 'Dest', { destinationName: 'MyDestination', role, - target: new BogusTarget() + targetArn: new Arn('arn:bogus') }); // WHEN @@ -56,8 +56,4 @@ export = { test.done(); } -}; - -class BogusTarget implements ILogDestinationTarget { - public readonly destinationTargetArn = new Arn('arn:bogus'); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/@aws-cdk/logs/test/test.loggroup.ts b/packages/@aws-cdk/logs/test/test.loggroup.ts index 8b5a95474c0f5..ee98e563bee80 100644 --- a/packages/@aws-cdk/logs/test/test.loggroup.ts +++ b/packages/@aws-cdk/logs/test/test.loggroup.ts @@ -10,7 +10,7 @@ export = { // WHEN new LogGroup(stack, 'LogGroup', { - retentionInDays: 7 + retentionDays: 7 }); // THEN @@ -30,7 +30,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::Logs::LogGroup', { - RetentionInDays: 365 + RetentionInDays: 730 })); test.done(); @@ -42,7 +42,7 @@ export = { // WHEN new LogGroup(stack, 'LogGroup', { - retentionInDays: Infinity + retentionDays: Infinity }); // THEN diff --git a/packages/@aws-cdk/logs/test/test.metricfilter.ts b/packages/@aws-cdk/logs/test/test.metricfilter.ts index cbf04f7921789..09e9d2ebb11be 100644 --- a/packages/@aws-cdk/logs/test/test.metricfilter.ts +++ b/packages/@aws-cdk/logs/test/test.metricfilter.ts @@ -1,8 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { LogGroup, LogPattern, } from '../lib'; -import { MetricFilter } from '../lib/metricfilter'; +import { FilterPattern, LogGroup, MetricFilter } from '../lib'; export = { 'trivial instantiation'(test: Test) { @@ -16,7 +15,7 @@ export = { metricNamespace: 'AWS/Test', metricName: 'Latency', metricValue: '$.latency', - logPattern: LogPattern.exists('$.latency') + filterPattern: FilterPattern.exists('$.latency') }); // THEN diff --git a/packages/@aws-cdk/logs/test/test.pattern.ts b/packages/@aws-cdk/logs/test/test.pattern.ts index 87c23caa9a566..04efe52c2197c 100644 --- a/packages/@aws-cdk/logs/test/test.pattern.ts +++ b/packages/@aws-cdk/logs/test/test.pattern.ts @@ -1,10 +1,10 @@ import { Test } from 'nodeunit'; -import { LogPattern } from '../lib'; +import { FilterPattern } from '../lib'; export = { 'text patterns': { 'simple text pattern'(test: Test) { - const pattern = LogPattern.allTerms('foo', 'bar', 'baz'); + const pattern = FilterPattern.allTerms('foo', 'bar', 'baz'); test.equal('"foo" "bar" "baz"', pattern.logPatternString); @@ -12,7 +12,7 @@ export = { }, 'quoted terms'(test: Test) { - const pattern = LogPattern.allTerms('"foo" he said'); + const pattern = FilterPattern.allTerms('"foo" he said'); test.equal('"\\"foo\\" he said"', pattern.logPatternString); @@ -20,7 +20,7 @@ export = { }, 'disjunction of conjunctions'(test: Test) { - const pattern = LogPattern.anyGroup( + const pattern = FilterPattern.anyTermGroup( ["foo", "bar"], ["baz"] ); @@ -31,7 +31,7 @@ export = { }, 'dont prefix with ? if only one disjunction'(test: Test) { - const pattern = LogPattern.anyGroup( + const pattern = FilterPattern.anyTermGroup( ["foo", "bar"] ); @@ -41,7 +41,7 @@ export = { }, 'empty log pattern is empty string'(test: Test) { - const pattern = LogPattern.anyGroup(); + const pattern = FilterPattern.anyTermGroup(); test.equal('', pattern.logPatternString); @@ -51,7 +51,7 @@ export = { 'json patterns': { 'string value'(test: Test) { - const pattern = LogPattern.stringValue('$.field', '=', 'value'); + const pattern = FilterPattern.stringValue('$.field', '=', 'value'); test.equal('{ $.field = "value" }', pattern.logPatternString); @@ -59,7 +59,7 @@ export = { }, 'also recognize =='(test: Test) { - const pattern = LogPattern.stringValue('$.field', '==', 'value'); + const pattern = FilterPattern.stringValue('$.field', '==', 'value'); test.equal('{ $.field = "value" }', pattern.logPatternString); @@ -67,7 +67,7 @@ export = { }, 'number patterns'(test: Test) { - const pattern = LogPattern.numberValue('$.field', '<=', 300); + const pattern = FilterPattern.numberValue('$.field', '<=', 300); test.equal('{ $.field <= 300 }', pattern.logPatternString); @@ -75,22 +75,22 @@ export = { }, 'combining with AND or OR'(test: Test) { - const p1 = LogPattern.numberValue('$.field', '<=', 300); - const p2 = LogPattern.stringValue('$.field', '=', 'value'); + const p1 = FilterPattern.numberValue('$.field', '<=', 300); + const p2 = FilterPattern.stringValue('$.field', '=', 'value'); - const andPattern = LogPattern.all(p1, p2); + const andPattern = FilterPattern.all(p1, p2); test.equal('{ ($.field <= 300) && ($.field = "value") }', andPattern.logPatternString); - const orPattern = LogPattern.any(p1, p2); + const orPattern = FilterPattern.any(p1, p2); test.equal('{ ($.field <= 300) || ($.field = "value") }', orPattern.logPatternString); test.done(); }, 'single AND is not wrapped with parens'(test: Test) { - const p1 = LogPattern.stringValue('$.field', '=', 'value'); + const p1 = FilterPattern.stringValue('$.field', '=', 'value'); - const pattern = LogPattern.all(p1); + const pattern = FilterPattern.all(p1); test.equal('{ $.field = "value" }', pattern.logPatternString); @@ -99,7 +99,7 @@ export = { 'empty AND is rejected'(test: Test) { test.throws(() => { - LogPattern.all(); + FilterPattern.all(); }); test.done(); @@ -107,14 +107,14 @@ export = { 'invalid string operators are rejected'(test: Test) { test.throws(() => { - LogPattern.stringValue('$.field', '<=', 'hello'); + FilterPattern.stringValue('$.field', '<=', 'hello'); }); test.done(); }, 'can test boolean value'(test: Test) { - const pattern = LogPattern.booleanValue('$.field', false); + const pattern = FilterPattern.booleanValue('$.field', false); test.equal('{ $.field IS FALSE }', pattern.logPatternString); @@ -124,7 +124,7 @@ export = { 'table patterns': { 'simple model'(test: Test) { - const pattern = LogPattern.spaceDelimited('...', 'status_code', 'bytes'); + const pattern = FilterPattern.spaceDelimited('...', 'status_code', 'bytes'); test.equal('[..., status_code, bytes]', pattern.logPatternString); @@ -132,7 +132,7 @@ export = { }, 'add restrictions'(test: Test) { - const pattern = LogPattern.spaceDelimited('...', 'status_code', 'bytes') + const pattern = FilterPattern.spaceDelimited('...', 'status_code', 'bytes') .whereString('status_code', '=', '4*') .whereNumber('status_code', '!=', 403); @@ -143,7 +143,7 @@ export = { 'cant use more than one ellipsis'(test: Test) { test.throws(() => { - LogPattern.spaceDelimited('...', 'status_code', '...'); + FilterPattern.spaceDelimited('...', 'status_code', '...'); }); test.done(); diff --git a/packages/@aws-cdk/logs/test/test.subscriptionfilter.ts b/packages/@aws-cdk/logs/test/test.subscriptionfilter.ts index d0dc37f737477..2fa2394e1dd60 100644 --- a/packages/@aws-cdk/logs/test/test.subscriptionfilter.ts +++ b/packages/@aws-cdk/logs/test/test.subscriptionfilter.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Arn, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { ISubscriptionDestination, LogGroup, LogPattern, SubscriptionDestinationProps, SubscriptionFilter } from '../lib'; +import { FilterPattern, ISubscriptionDestination, LogGroup, SubscriptionFilter } from '../lib'; export = { 'trivial instantiation'(test: Test) { @@ -13,7 +13,7 @@ export = { new SubscriptionFilter(stack, 'Subscription', { logGroup, destination: new FakeDestination(), - logPattern: LogPattern.literal("some pattern") + filterPattern: FilterPattern.literal("some pattern") }); // THEN @@ -28,7 +28,9 @@ export = { }; class FakeDestination implements ISubscriptionDestination { - public readonly subscriptionDestinationProps: SubscriptionDestinationProps = { - arn: new Arn('arn:bogus'), - }; + public subscriptionDestination(_sourceLogGroup: LogGroup) { + return { + arn: new Arn('arn:bogus'), + }; + } } \ No newline at end of file