Skip to content

Commit fcf981b

Browse files
authored
feat(logs): add QueryDefinition L2 Construct (#18655)
This PR implemented the [[feature request] (aws-cloudwatch): Saved queries](#16395). It will let users be able to create CloudWatch Logs Insights QueryDefinition by using L2 construct. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3ce40b4 commit fcf981b

File tree

9 files changed

+434
-0
lines changed

9 files changed

+434
-0
lines changed

packages/@aws-cdk/aws-logs/README.md

+17
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,23 @@ const pattern = logs.FilterPattern.spaceDelimited('time', 'component', '...', 'r
306306
.whereNumber('result_code', '!=', 200);
307307
```
308308

309+
## Logs Insights Query Definition
310+
311+
Creates a query definition for CloudWatch Logs Insights.
312+
313+
Example:
314+
315+
```ts
316+
new logs.QueryDefinition(this, 'QueryDefinition', {
317+
queryDefinitionName: 'MyQuery',
318+
queryString: new logs.QueryString({
319+
fields: ['@timestamp', '@message'],
320+
sort: '@timestamp desc',
321+
limit: 20,
322+
}),
323+
});
324+
```
325+
309326
## Notes
310327

311328
Be aware that Log Group ARNs will always have the string `:*` appended to

packages/@aws-cdk/aws-logs/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './pattern';
66
export * from './subscription-filter';
77
export * from './log-retention';
88
export * from './policy';
9+
export * from './query-definition';
910

1011
// AWS::Logs CloudFormation Resources:
1112
export * from './logs.generated';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { Resource } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
import { CfnQueryDefinition } from '.';
4+
import { ILogGroup } from './log-group';
5+
6+
7+
/**
8+
* Properties for a QueryString
9+
*/
10+
export interface QueryStringProps {
11+
/**
12+
* Retrieves the specified fields from log events for display.
13+
*
14+
* @default - no fields in QueryString
15+
*/
16+
readonly fields?: string[];
17+
18+
/**
19+
* Extracts data from a log field and creates one or more ephemeral fields that you can process further in the query.
20+
*
21+
* @default - no parse in QueryString
22+
*/
23+
readonly parse?: string;
24+
25+
/**
26+
* Filters the results of a query that's based on one or more conditions.
27+
*
28+
* @default - no filter in QueryString
29+
*/
30+
readonly filter?: string;
31+
32+
/**
33+
* Uses log field values to calculate aggregate statistics.
34+
*
35+
* @default - no stats in QueryString
36+
*/
37+
readonly stats?: string;
38+
39+
/**
40+
* Sorts the retrieved log events.
41+
*
42+
* @default - no sort in QueryString
43+
*/
44+
readonly sort?: string;
45+
46+
/**
47+
* Specifies the number of log events returned by the query.
48+
*
49+
* @default - no limit in QueryString
50+
*/
51+
readonly limit?: Number;
52+
53+
/**
54+
* Specifies which fields to display in the query results.
55+
*
56+
* @default - no display in QueryString
57+
*/
58+
readonly display?: string;
59+
}
60+
61+
interface QueryStringMap {
62+
readonly fields?: string,
63+
readonly parse?: string,
64+
readonly filter?: string,
65+
readonly stats?: string,
66+
readonly sort?: string,
67+
readonly limit?: Number,
68+
readonly display?: string,
69+
}
70+
71+
/**
72+
* Define a QueryString
73+
*/
74+
export class QueryString {
75+
private readonly fields?: string[];
76+
private readonly parse?: string;
77+
private readonly filter?: string;
78+
private readonly stats?: string;
79+
private readonly sort?: string;
80+
private readonly limit?: Number;
81+
private readonly display?: string;
82+
83+
constructor(props: QueryStringProps = {}) {
84+
this.fields = props.fields;
85+
this.parse = props.parse;
86+
this.filter = props.filter;
87+
this.stats = props.stats;
88+
this.sort = props.sort;
89+
this.limit = props.limit;
90+
this.display = props.display;
91+
}
92+
93+
/**
94+
* String representation of this QueryString.
95+
*/
96+
public toString(): string {
97+
return noUndef({
98+
fields: this.fields !== undefined ? this.fields.join(', ') : this.fields,
99+
parse: this.parse,
100+
filter: this.filter,
101+
stats: this.stats,
102+
sort: this.sort,
103+
limit: this.limit,
104+
display: this.display,
105+
}).join(' | ');
106+
}
107+
}
108+
109+
function noUndef(x: QueryStringMap): string[] {
110+
const ret: string[] = [];
111+
for (const [key, value] of Object.entries(x)) {
112+
if (value !== undefined) {
113+
ret.push(`${key} ${value}`);
114+
}
115+
}
116+
return ret;
117+
}
118+
119+
/**
120+
* Properties for a QueryDefinition
121+
*/
122+
export interface QueryDefinitionProps {
123+
/**
124+
* Name of the query definition.
125+
*/
126+
readonly queryDefinitionName: string;
127+
128+
/**
129+
* The query string to use for this query definition.
130+
*/
131+
readonly queryString: QueryString;
132+
133+
/**
134+
* Specify certain log groups for the query definition.
135+
*
136+
* @default - no specified log groups
137+
*/
138+
readonly logGroups?: ILogGroup[];
139+
}
140+
141+
/**
142+
* Define a query definition for CloudWatch Logs Insights
143+
*/
144+
export class QueryDefinition extends Resource {
145+
/**
146+
* The ID of the query definition.
147+
*
148+
* @attribute
149+
*/
150+
public readonly queryDefinitionId: string;
151+
152+
constructor(scope: Construct, id: string, props: QueryDefinitionProps) {
153+
super(scope, id, {
154+
physicalName: props.queryDefinitionName,
155+
});
156+
157+
const queryDefinition = new CfnQueryDefinition(this, 'Resource', {
158+
name: props.queryDefinitionName,
159+
queryString: props.queryString.toString(),
160+
logGroupNames: typeof props.logGroups === 'undefined' ? [] : props.logGroups.flatMap(logGroup => logGroup.logGroupName),
161+
});
162+
163+
this.queryDefinitionId = queryDefinition.attrQueryDefinitionId;
164+
}
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core';
2+
import { LogGroup, QueryDefinition, QueryString } from '../lib';
3+
4+
class LogsInsightsQueryDefinitionIntegStack extends Stack {
5+
constructor(scope: App, id: string, props?: StackProps) {
6+
super(scope, id, props);
7+
8+
const logGroup = new LogGroup(this, 'LogGroup', {
9+
removalPolicy: RemovalPolicy.DESTROY,
10+
});
11+
12+
new QueryDefinition(this, 'QueryDefinition', {
13+
queryDefinitionName: 'QueryDefinition',
14+
queryString: new QueryString({
15+
fields: ['@timestamp', '@message'],
16+
parse: '@message "[*] *" as loggingType, loggingMessage',
17+
filter: 'loggingType = "ERROR"',
18+
sort: '@timestamp desc',
19+
limit: 20,
20+
display: 'loggingMessage',
21+
}),
22+
logGroups: [logGroup],
23+
});
24+
}
25+
}
26+
27+
const app = new App();
28+
new LogsInsightsQueryDefinitionIntegStack(app, 'aws-cdk-logs-querydefinition-integ');
29+
app.synth();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import { Stack } from '@aws-cdk/core';
3+
import { LogGroup, QueryDefinition, QueryString } from '../lib';
4+
5+
describe('query definition', () => {
6+
test('create a query definition', () => {
7+
// GIVEN
8+
const stack = new Stack();
9+
10+
// WHEN
11+
new QueryDefinition(stack, 'QueryDefinition', {
12+
queryDefinitionName: 'MyQuery',
13+
queryString: new QueryString({
14+
fields: ['@timestamp', '@message'],
15+
sort: '@timestamp desc',
16+
limit: 20,
17+
}),
18+
});
19+
20+
// THEN
21+
Template.fromStack(stack).hasResourceProperties('AWS::Logs::QueryDefinition', {
22+
Name: 'MyQuery',
23+
QueryString: 'fields @timestamp, @message | sort @timestamp desc | limit 20',
24+
});
25+
});
26+
27+
test('create a query definition against certain log groups', () => {
28+
// GIVEN
29+
const stack = new Stack();
30+
31+
// WHEN
32+
const logGroup = new LogGroup(stack, 'MyLogGroup');
33+
34+
new QueryDefinition(stack, 'QueryDefinition', {
35+
queryDefinitionName: 'MyQuery',
36+
queryString: new QueryString({
37+
fields: ['@timestamp', '@message'],
38+
sort: '@timestamp desc',
39+
limit: 20,
40+
}),
41+
logGroups: [logGroup],
42+
});
43+
44+
// THEN
45+
Template.fromStack(stack).hasResourceProperties('AWS::Logs::QueryDefinition', {
46+
Name: 'MyQuery',
47+
QueryString: 'fields @timestamp, @message | sort @timestamp desc | limit 20',
48+
LogGroupNames: [{ Ref: 'MyLogGroup5C0DAD85' }],
49+
});
50+
});
51+
52+
test('create a query definition with all commands', () => {
53+
// GIVEN
54+
const stack = new Stack();
55+
56+
// WHEN
57+
const logGroup = new LogGroup(stack, 'MyLogGroup');
58+
59+
new QueryDefinition(stack, 'QueryDefinition', {
60+
queryDefinitionName: 'MyQuery',
61+
queryString: new QueryString({
62+
fields: ['@timestamp', '@message'],
63+
parse: '@message "[*] *" as loggingType, loggingMessage',
64+
filter: 'loggingType = "ERROR"',
65+
sort: '@timestamp desc',
66+
limit: 20,
67+
display: 'loggingMessage',
68+
}),
69+
logGroups: [logGroup],
70+
});
71+
72+
// THEN
73+
Template.fromStack(stack).hasResourceProperties('AWS::Logs::QueryDefinition', {
74+
Name: 'MyQuery',
75+
QueryString: 'fields @timestamp, @message | parse @message "[*] *" as loggingType, loggingMessage | filter loggingType = "ERROR" | sort @timestamp desc | limit 20 | display loggingMessage',
76+
});
77+
});
78+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"Resources": {
3+
"LogGroupF5B46931": {
4+
"Type": "AWS::Logs::LogGroup",
5+
"Properties": {
6+
"RetentionInDays": 731
7+
},
8+
"UpdateReplacePolicy": "Delete",
9+
"DeletionPolicy": "Delete"
10+
},
11+
"QueryDefinition4190BC36": {
12+
"Type": "AWS::Logs::QueryDefinition",
13+
"Properties": {
14+
"Name": "QueryDefinition",
15+
"QueryString": "fields @timestamp, @message | parse @message \"[*] *\" as loggingType, loggingMessage | filter loggingType = \"ERROR\" | sort @timestamp desc | limit 20 | display loggingMessage",
16+
"LogGroupNames": [
17+
{
18+
"Ref": "LogGroupF5B46931"
19+
}
20+
]
21+
}
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"17.0.0"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": "17.0.0",
3+
"artifacts": {
4+
"Tree": {
5+
"type": "cdk:tree",
6+
"properties": {
7+
"file": "tree.json"
8+
}
9+
},
10+
"aws-cdk-logs-querydefinition-integ": {
11+
"type": "aws:cloudformation:stack",
12+
"environment": "aws://unknown-account/unknown-region",
13+
"properties": {
14+
"templateFile": "aws-cdk-logs-querydefinition-integ.template.json",
15+
"validateOnSynth": false
16+
},
17+
"metadata": {
18+
"/aws-cdk-logs-querydefinition-integ/LogGroup/Resource": [
19+
{
20+
"type": "aws:cdk:logicalId",
21+
"data": "LogGroupF5B46931"
22+
}
23+
],
24+
"/aws-cdk-logs-querydefinition-integ/QueryDefinition/Resource": [
25+
{
26+
"type": "aws:cdk:logicalId",
27+
"data": "QueryDefinition4190BC36"
28+
}
29+
]
30+
},
31+
"displayName": "aws-cdk-logs-querydefinition-integ"
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)