Skip to content

Commit c4b00be

Browse files
authored
feat(appmesh): access log format support for app mesh (#25229)
**Implement access logging feature for AppMesh CDK.** * Access logging feature allows customer to choose between Text and Json format for the virtual nodes and virtual gateways. Customers can also specify patterns supported by Envoy. (https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage) Example CFN template: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appmesh-virtualnode-loggingformat.html ### All Submissions: * [Y] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [N] 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 * [Y] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [Y] 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*
1 parent 8854739 commit c4b00be

File tree

9 files changed

+402
-10
lines changed

9 files changed

+402
-10
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"validateOnSynth": false,
1818
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
1919
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
20-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402.json",
2121
"requiresBootstrapStackVersion": 6,
2222
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2323
"additionalDependencies": [

packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.assets.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"version": "31.0.0",
33
"files": {
4-
"e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb": {
4+
"66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402": {
55
"source": {
66
"path": "mesh-stack.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb.json",
12+
"objectKey": "66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.template.json

+15
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,9 @@
10801080
"Logging": {
10811081
"AccessLog": {
10821082
"File": {
1083+
"Format": {
1084+
"Text": "test_pattern"
1085+
},
10831086
"Path": "/dev/stdout"
10841087
}
10851088
}
@@ -1178,6 +1181,18 @@
11781181
"Logging": {
11791182
"AccessLog": {
11801183
"File": {
1184+
"Format": {
1185+
"Json": [
1186+
{
1187+
"Key": "testKey1",
1188+
"Value": "testValue1"
1189+
},
1190+
{
1191+
"Key": "testKey2",
1192+
"Value": "testValue2"
1193+
}
1194+
]
1195+
},
11811196
"Path": "/dev/stdout"
11821197
}
11831198
}

packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/tree.json

+17-2
Original file line numberDiff line numberDiff line change
@@ -1604,7 +1604,10 @@
16041604
"logging": {
16051605
"accessLog": {
16061606
"file": {
1607-
"path": "/dev/stdout"
1607+
"path": "/dev/stdout",
1608+
"format": {
1609+
"text": "test_pattern"
1610+
}
16081611
}
16091612
}
16101613
}
@@ -1721,7 +1724,19 @@
17211724
"logging": {
17221725
"accessLog": {
17231726
"file": {
1724-
"path": "/dev/stdout"
1727+
"path": "/dev/stdout",
1728+
"format": {
1729+
"json": [
1730+
{
1731+
"key": "testKey1",
1732+
"value": "testValue1"
1733+
},
1734+
{
1735+
"key": "testKey2",
1736+
"value": "testValue2"
1737+
}
1738+
]
1739+
}
17251740
}
17261741
}
17271742
}

packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const node3 = mesh.addVirtualNode('node3', {
114114
},
115115
},
116116
},
117-
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
117+
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')),
118118
});
119119

120120
const node4 = mesh.addVirtualNode('node4', {
@@ -145,7 +145,9 @@ const node4 = mesh.addVirtualNode('node4', {
145145
},
146146
},
147147
},
148-
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
148+
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout',
149+
appmesh.LoggingFormat.fromJson(
150+
{ testKey1: 'testValue1', testKey2: 'testValue2' })),
149151
});
150152

151153
node4.addBackend(appmesh.Backend.virtualService(

packages/aws-cdk-lib/aws-appmesh/README.md

+41
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,47 @@ const node = new appmesh.VirtualNode(this, 'node', {
197197
cdk.Tags.of(node).add('Environment', 'Dev');
198198
```
199199

200+
Create a `VirtualNode` with the customized access logging format.
201+
202+
```ts
203+
declare const mesh: appmesh.Mesh;
204+
declare const service: cloudmap.Service;
205+
const node = new appmesh.VirtualNode(this, 'node', {
206+
mesh,
207+
serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service),
208+
listeners: [appmesh.VirtualNodeListener.http({
209+
port: 8080,
210+
healthCheck: appmesh.HealthCheck.http({
211+
healthyThreshold: 3,
212+
interval: cdk.Duration.seconds(5),
213+
path: '/ping',
214+
timeout: cdk.Duration.seconds(2),
215+
unhealthyThreshold: 2,
216+
}),
217+
timeout: {
218+
idle: cdk.Duration.seconds(5),
219+
},
220+
})],
221+
backendDefaults: {
222+
tlsClientPolicy: {
223+
validation: {
224+
trust: appmesh.TlsValidationTrust.file('/keys/local_cert_chain.pem'),
225+
},
226+
},
227+
},
228+
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout',
229+
appmesh.LoggingFormat.fromJson(
230+
{testKey1: 'testValue1', testKey2: 'testValue2'})),
231+
});
232+
```
233+
234+
By using a key-value pair indexed signature, you can specify json key pairs to customize the log entry pattern. You can also use text format as below. You can only specify one of these 2 formats.
235+
236+
```ts
237+
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')),
238+
```
239+
240+
For what values and operators you can use for these two formats, please visit the latest envoy documentation. (https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage)
200241
Create a `VirtualNode` with the constructor and add backend virtual service.
201242

202243
```ts

packages/aws-cdk-lib/aws-appmesh/lib/shared-interfaces.ts

+91-3
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ export abstract class AccessLog {
122122
*
123123
* @default - no file based access logging
124124
*/
125-
public static fromFilePath(filePath: string): AccessLog {
126-
return new FileAccessLog(filePath);
125+
public static fromFilePath(filePath: string, loggingFormat?: LoggingFormat): AccessLog {
126+
return new FileAccessLog(filePath, loggingFormat);
127127
}
128128

129129
/**
@@ -143,28 +143,116 @@ class FileAccessLog extends AccessLog {
143143
* @default - no file based access logging
144144
*/
145145
public readonly filePath: string;
146+
private readonly virtualNodeLoggingFormat?: CfnVirtualNode.LoggingFormatProperty;
147+
private readonly virtualGatewayLoggingFormat?: CfnVirtualGateway.LoggingFormatProperty;
146148

147-
constructor(filePath: string) {
149+
constructor(filePath: string, loggingFormat?: LoggingFormat) {
148150
super();
149151
this.filePath = filePath;
152+
// For now we have the same setting for Virtual Gateway and Virtual Nodes
153+
this.virtualGatewayLoggingFormat = loggingFormat?.bind().formatConfig;
154+
this.virtualNodeLoggingFormat = loggingFormat?.bind().formatConfig;
150155
}
151156

152157
public bind(_scope: Construct): AccessLogConfig {
153158
return {
154159
virtualNodeAccessLog: {
155160
file: {
156161
path: this.filePath,
162+
format: this.virtualNodeLoggingFormat,
157163
},
158164
},
159165
virtualGatewayAccessLog: {
160166
file: {
161167
path: this.filePath,
168+
format: this.virtualGatewayLoggingFormat,
162169
},
163170
},
164171
};
165172
}
166173
}
167174

175+
/**
176+
* All Properties for Envoy Access Logging Format for mesh endpoints
177+
*/
178+
export interface LoggingFormatConfig {
179+
/**
180+
* CFN configuration for Access Logging Format
181+
*
182+
* @default - no access logging format
183+
*/
184+
readonly formatConfig?: CfnVirtualNode.LoggingFormatProperty;
185+
}
186+
187+
/**
188+
* Configuration for Envoy Access Logging Format for mesh endpoints
189+
*/
190+
export abstract class LoggingFormat {
191+
/**
192+
* Generate logging format from text pattern
193+
*/
194+
public static fromText(text: string): LoggingFormat {
195+
return new TextLoggingFormat(text);
196+
}
197+
/**
198+
* Generate logging format from json key pairs
199+
*/
200+
public static fromJson(jsonLoggingFormat :{[key:string]: string}): LoggingFormat {
201+
if (Object.keys(jsonLoggingFormat).length == 0) {
202+
throw new Error('Json key pairs cannot be empty.');
203+
}
204+
205+
return new JsonLoggingFormat(jsonLoggingFormat);
206+
};
207+
208+
/**
209+
* Called when the Access Log Format is initialized. Can be used to enforce
210+
* mutual exclusivity with future properties
211+
*/
212+
public abstract bind(): LoggingFormatConfig;
213+
}
214+
215+
/**
216+
* Configuration for Json logging format
217+
*/
218+
class JsonLoggingFormat extends LoggingFormat {
219+
/**
220+
* Json pattern for the output logs
221+
*/
222+
private readonly json: Array<CfnVirtualNode.JsonFormatRefProperty>;
223+
constructor(json: {[key:string]: string}) {
224+
super();
225+
this.json = Object.entries(json).map(([key, value]) => ({ key, value }));
226+
}
227+
228+
bind(): LoggingFormatConfig {
229+
return {
230+
formatConfig: {
231+
json: this.json,
232+
},
233+
};
234+
}
235+
}
236+
237+
class TextLoggingFormat extends LoggingFormat {
238+
/**
239+
* Json pattern for the output logs
240+
*/
241+
private readonly text: string;
242+
constructor(text: string) {
243+
super();
244+
this.text = text;
245+
}
246+
247+
public bind(): LoggingFormatConfig {
248+
return {
249+
formatConfig: {
250+
text: this.text,
251+
},
252+
};
253+
}
254+
}
255+
168256
/**
169257
* Represents the properties needed to define backend defaults
170258
*/

0 commit comments

Comments
 (0)