Skip to content

Commit

Permalink
fix(events-targets): cloudwatch logs requires specific input template (
Browse files Browse the repository at this point in the history
…#20748)

The CloudWatch logs log group target requires a very specific input
template. It does not support `input` or `inputPath` and if
`inputTemplate` is specified it must be in the format of

`{"timestamp": <time>, "message": <message>}`

where both the values for `timestamp` and `message` are strings.

This requirement is not very well documented, the only reference I could
find is in a `Note` on this
[page](https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutTargets.html).

This PR adds a new property `logEvent` and deprecates the `event`
property to ensure that if the user adds an event input that it uses the
correct format. While working on this PR is started by adding some
validation if the user provides the `event` property and then went down
the route of providing the new `logEvent` property. Since I already did
the work of creating the validation for `event` I've kept it, but I'm
open to just removing that logic since it is validating a `deprecated`
property.

I've also added an integration test that asserts that the expected
message is written to the log group.

fixes #19451


----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall committed Jun 20, 2022
1 parent 454d60f commit 26ff3c7
Show file tree
Hide file tree
Showing 14 changed files with 1,886 additions and 382 deletions.
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,39 @@ const rule = new events.Rule(this, 'rule', {
rule.addTarget(new targets.CloudWatchLogGroup(logGroup));
```

A rule target input can also be specified to modify the event that is sent to the log group.
Unlike other event targets, CloudWatchLogs requires a specific input template format.

```ts
import * as logs from '@aws-cdk/aws-logs';
declare const logGroup: logs.LogGroup;
declare const rule: events.Rule;

rule.addTarget(new targets.CloudWatchLogGroup(logGroup, {
logEvent: targets.LogGroupTargetInput({
timestamp: events.EventField.from('$.time'),
message: events.EventField.from('$.detail-type'),
}),
}));
```

If you want to use static values to overwrite the `message` make sure that you provide a `string`
value.

```ts
import * as logs from '@aws-cdk/aws-logs';
declare const logGroup: logs.LogGroup;
declare const rule: events.Rule;

rule.addTarget(new targets.CloudWatchLogGroup(logGroup, {
logEvent: targets.LogGroupTargetInput({
message: JSON.stringify({
CustomField: 'CustomValue',
}),
}),
}));
```

## Start a CodeBuild build

Use the `CodeBuildProject` target to trigger a CodeBuild project.
Expand Down
108 changes: 106 additions & 2 deletions packages/@aws-cdk/aws-events-targets/lib/log-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,59 @@ import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';
import * as cdk from '@aws-cdk/core';
import { ArnFormat } from '@aws-cdk/core';
import { ArnFormat, Stack } from '@aws-cdk/core';
import { LogGroupResourcePolicy } from './log-group-resource-policy';
import { TargetBaseProps, bindBaseTargetConfig } from './util';
import { RuleTargetInputProperties, RuleTargetInput, EventField, IRule } from '@aws-cdk/aws-events';

/**
* Options used when creating a target input template
*/
export interface LogGroupTargetInputOptions {
/**
* The timestamp that will appear in the CloudWatch Logs record
*
* @default EventField.time
*/
readonly timestamp?: any;

/**
* The value provided here will be used in the Log "message" field.
*
* This field must be a string. If an object is passed (e.g. JSON data)
* it will not throw an error, but the message that makes it to
* CloudWatch logs will be incorrect. This is a likely scenario if
* doing something like: EventField.fromPath('$.detail') since in most cases
* the `detail` field contains JSON data.
*
* @default EventField.detailType
*/
readonly message?: any;
}

/**
* The input to send to the CloudWatch LogGroup target
*/
export abstract class LogGroupTargetInput {

/**
* Pass a JSON object to the the log group event target
*
* May contain strings returned by `EventField.from()` to substitute in parts of the
* matched event.
*/
public static fromObject(options?: LogGroupTargetInputOptions): RuleTargetInput {
return RuleTargetInput.fromObject({
timestamp: options?.timestamp ?? EventField.time,
message: options?.message ?? EventField.detailType,
});
};

/**
* Return the input properties for this input object
*/
public abstract bind(rule: IRule): RuleTargetInputProperties;
}

/**
* Customize the CloudWatch LogGroup Event Target
Expand All @@ -16,14 +66,25 @@ export interface LogGroupProps extends TargetBaseProps {
* This will be the event logged into the CloudWatch LogGroup
*
* @default - the entire EventBridge event
* @deprecated use logEvent instead
*/
readonly event?: events.RuleTargetInput;

/**
* The event to send to the CloudWatch LogGroup
*
* This will be the event logged into the CloudWatch LogGroup
*
* @default - the entire EventBridge event
*/
readonly logEvent?: LogGroupTargetInput;
}

/**
* Use an AWS CloudWatch LogGroup as an event rule target.
*/
export class CloudWatchLogGroup implements events.IRuleTarget {
private target?: RuleTargetInputProperties;
constructor(private readonly logGroup: logs.ILogGroup, private readonly props: LogGroupProps = {}) {}

/**
Expand All @@ -35,6 +96,17 @@ export class CloudWatchLogGroup implements events.IRuleTarget {

const logGroupStack = cdk.Stack.of(this.logGroup);

if (this.props.event && this.props.logEvent) {
throw new Error('Only one of "event" or "logEvent" can be specified');
}

this.target = this.props.event?.bind(_rule);
if (this.target?.inputPath || this.target?.input) {
throw new Error('CloudWatchLogGroup targets does not support input or inputPath');
}

_rule.node.addValidation({ validate: () => this.validateInputTemplate() });

if (!this.logGroup.node.tryFindChild(resourcePolicyId)) {
new LogGroupResourcePolicy(logGroupStack, resourcePolicyId, {
policyStatements: [new iam.PolicyStatement({
Expand All @@ -54,8 +126,40 @@ export class CloudWatchLogGroup implements events.IRuleTarget {
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
resourceName: this.logGroup.logGroupName,
}),
input: this.props.event,
input: this.props.event ?? this.props.logEvent,
targetResource: this.logGroup,
};
}

/**
* Validate that the target event input template has the correct format.
* The CloudWatchLogs target only supports a template with the format of:
* {"timestamp": <time>, "message": <message>}
*
* This is only needed if the deprecated `event` property is used.
*
* @see https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutTargets.html
*/
private validateInputTemplate(): string[] {
if (this.target?.inputTemplate) {
const resolvedTemplate = Stack.of(this.logGroup).resolve(this.target.inputTemplate);
if (typeof(resolvedTemplate) === 'string') {
// need to add the quotes back to the string so that we can parse the json
// '{"timestamp": <time>}' -> '{"timestamp": "<time>"}'
const quotedTemplate = resolvedTemplate.replace(new RegExp('(\<.*?\>)', 'g'), '"$1"');
try {
const inputTemplate = JSON.parse(quotedTemplate);
const inputTemplateKeys = Object.keys(inputTemplate);
if (inputTemplateKeys.length !== 2 ||
(!inputTemplateKeys.includes('timestamp') || !inputTemplateKeys.includes('message'))) {
return ['CloudWatchLogGroup targets only support input templates in the format {timestamp: <timestamp>, message: <message>}'];
}
} catch (e) {
return ['Could not parse input template as JSON.\n' +
'CloudWatchLogGroup targets only support input templates in the format {timestamp: <timestamp>, message: <message>}', e];
}
}
}
return [];
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@aws-cdk/aws-codecommit": "0.0.0",
"@aws-cdk/aws-s3": "0.0.0",
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@types/jest": "^27.5.2",
Expand Down
48 changes: 41 additions & 7 deletions packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import * as logs from '@aws-cdk/aws-logs';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
import * as targets from '../../lib';
import { IntegTest, ExpectedResult, AssertionsProvider } from '@aws-cdk/integ-tests';
import { LogGroupTargetInput } from '../../lib';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'log-group-events');

const logGroup = new logs.LogGroup(stack, 'log-group', {
logGroupName: 'MyLogGroupName',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

const logGroup2 = new logs.LogGroup(stack, 'log-group2', {
logGroupName: 'MyLogGroupName2',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

Expand All @@ -31,12 +31,15 @@ const timer = new events.Rule(stack, 'Timer', {
});
timer.addTarget(new targets.CloudWatchLogGroup(logGroup));

const timer2 = new events.Rule(stack, 'Timer2', {
schedule: events.Schedule.rate(cdk.Duration.minutes(2)),
const customRule = new events.Rule(stack, 'CustomRule', {
eventPattern: {
source: ['cdk-integ'],
detailType: ['cdk-integ-custom-rule'],
},
});
timer2.addTarget(new targets.CloudWatchLogGroup(logGroup2, {
event: events.RuleTargetInput.fromObject({
data: events.EventField.fromPath('$.detail-type'),
customRule.addTarget(new targets.CloudWatchLogGroup(logGroup2, {
logEvent: LogGroupTargetInput.fromObject({
message: events.EventField.fromPath('$.detail.date'),
}),
}));

Expand All @@ -51,5 +54,36 @@ timer3.addTarget(new targets.CloudWatchLogGroup(importedLogGroup, {
retryAttempts: 2,
}));

const integ = new IntegTest(app, 'LogGroup', {
testCases: [stack],
});

const putEventsDate = Date.now().toString();
const expectedValue = `abc${putEventsDate}`;

const putEvent = integ.assertions.awsApiCall('EventBridge', 'putEvents', {
Entries: [
{
Detail: JSON.stringify({
date: expectedValue,
}),
DetailType: 'cdk-integ-custom-rule',
Source: 'cdk-integ',
},
],
});
const assertionProvider = putEvent.node.tryFindChild('SdkProvider') as AssertionsProvider;
assertionProvider.addPolicyStatementFromSdkCall('events', 'PutEvents');

const logEvents = integ.assertions.awsApiCall('CloudWatchLogs', 'filterLogEvents', {
logGroupName: logGroup2.logGroupName,
startTime: putEventsDate,
limit: 1,
});

logEvents.node.addDependency(putEvent);

logEvents.assertAtPath('events.0.message', ExpectedResult.stringLikeRegexp(expectedValue));

app.synth();

0 comments on commit 26ff3c7

Please sign in to comment.