diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/manifest.json index e7ee4a2d13956..d2b522c36c42b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/manifest.json @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.assets.json index d0642d70c4e9a..a2a4fe1e730cb 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.assets.json @@ -1,7 +1,7 @@ { "version": "31.0.0", "files": { - "e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb": { + "66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402": { "source": { "path": "mesh-stack.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "e22d0f948e4b9a0aac11f68f048f52c09eeb7d8fef79972eb7e6f957111955bb.json", + "objectKey": "66ab683818060e1118fc673956ded609e269963fadf52391a2f080831d022402.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.template.json index d73e720559915..b4a4529cf5052 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/mesh-stack.template.json @@ -1080,6 +1080,9 @@ "Logging": { "AccessLog": { "File": { + "Format": { + "Text": "test_pattern" + }, "Path": "/dev/stdout" } } @@ -1178,6 +1181,18 @@ "Logging": { "AccessLog": { "File": { + "Format": { + "Json": [ + { + "Key": "testKey1", + "Value": "testValue1" + }, + { + "Key": "testKey2", + "Value": "testValue2" + } + ] + }, "Path": "/dev/stdout" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/tree.json index 0b184ce873b44..72752383227f5 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.js.snapshot/tree.json @@ -1604,7 +1604,10 @@ "logging": { "accessLog": { "file": { - "path": "/dev/stdout" + "path": "/dev/stdout", + "format": { + "text": "test_pattern" + } } } } @@ -1721,7 +1724,19 @@ "logging": { "accessLog": { "file": { - "path": "/dev/stdout" + "path": "/dev/stdout", + "format": { + "json": [ + { + "key": "testKey1", + "value": "testValue1" + }, + { + "key": "testKey2", + "value": "testValue2" + } + ] + } } } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.ts index 9048a0fbfbd22..65fde7cad7f42 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appmesh/test/integ.mesh.ts @@ -114,7 +114,7 @@ const node3 = mesh.addVirtualNode('node3', { }, }, }, - accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')), }); const node4 = mesh.addVirtualNode('node4', { @@ -145,7 +145,9 @@ const node4 = mesh.addVirtualNode('node4', { }, }, }, - accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', + appmesh.LoggingFormat.fromJson( + { testKey1: 'testValue1', testKey2: 'testValue2' })), }); node4.addBackend(appmesh.Backend.virtualService( diff --git a/packages/aws-cdk-lib/aws-appmesh/README.md b/packages/aws-cdk-lib/aws-appmesh/README.md index 846bc0cf8f2f2..7bab23e235586 100644 --- a/packages/aws-cdk-lib/aws-appmesh/README.md +++ b/packages/aws-cdk-lib/aws-appmesh/README.md @@ -197,6 +197,47 @@ const node = new appmesh.VirtualNode(this, 'node', { cdk.Tags.of(node).add('Environment', 'Dev'); ``` +Create a `VirtualNode` with the customized access logging format. + +```ts +declare const mesh: appmesh.Mesh; +declare const service: cloudmap.Service; +const node = new appmesh.VirtualNode(this, 'node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), + listeners: [appmesh.VirtualNodeListener.http({ + port: 8080, + healthCheck: appmesh.HealthCheck.http({ + healthyThreshold: 3, + interval: cdk.Duration.seconds(5), + path: '/ping', + timeout: cdk.Duration.seconds(2), + unhealthyThreshold: 2, + }), + timeout: { + idle: cdk.Duration.seconds(5), + }, + })], + backendDefaults: { + tlsClientPolicy: { + validation: { + trust: appmesh.TlsValidationTrust.file('/keys/local_cert_chain.pem'), + }, + }, + }, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', + appmesh.LoggingFormat.fromJson( + {testKey1: 'testValue1', testKey2: 'testValue2'})), +}); +``` + +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. + +```ts + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')), +``` + +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) Create a `VirtualNode` with the constructor and add backend virtual service. ```ts diff --git a/packages/aws-cdk-lib/aws-appmesh/lib/shared-interfaces.ts b/packages/aws-cdk-lib/aws-appmesh/lib/shared-interfaces.ts index 9edc7438139e5..717b701a20da8 100644 --- a/packages/aws-cdk-lib/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/aws-cdk-lib/aws-appmesh/lib/shared-interfaces.ts @@ -122,8 +122,8 @@ export abstract class AccessLog { * * @default - no file based access logging */ - public static fromFilePath(filePath: string): AccessLog { - return new FileAccessLog(filePath); + public static fromFilePath(filePath: string, loggingFormat?: LoggingFormat): AccessLog { + return new FileAccessLog(filePath, loggingFormat); } /** @@ -143,10 +143,15 @@ class FileAccessLog extends AccessLog { * @default - no file based access logging */ public readonly filePath: string; + private readonly virtualNodeLoggingFormat?: CfnVirtualNode.LoggingFormatProperty; + private readonly virtualGatewayLoggingFormat?: CfnVirtualGateway.LoggingFormatProperty; - constructor(filePath: string) { + constructor(filePath: string, loggingFormat?: LoggingFormat) { super(); this.filePath = filePath; + // For now we have the same setting for Virtual Gateway and Virtual Nodes + this.virtualGatewayLoggingFormat = loggingFormat?.bind().formatConfig; + this.virtualNodeLoggingFormat = loggingFormat?.bind().formatConfig; } public bind(_scope: Construct): AccessLogConfig { @@ -154,17 +159,100 @@ class FileAccessLog extends AccessLog { virtualNodeAccessLog: { file: { path: this.filePath, + format: this.virtualNodeLoggingFormat, }, }, virtualGatewayAccessLog: { file: { path: this.filePath, + format: this.virtualGatewayLoggingFormat, }, }, }; } } +/** + * All Properties for Envoy Access Logging Format for mesh endpoints + */ +export interface LoggingFormatConfig { + /** + * CFN configuration for Access Logging Format + * + * @default - no access logging format + */ + readonly formatConfig?: CfnVirtualNode.LoggingFormatProperty; +} + +/** + * Configuration for Envoy Access Logging Format for mesh endpoints + */ +export abstract class LoggingFormat { + /** + * Generate logging format from text pattern + */ + public static fromText(text: string): LoggingFormat { + return new TextLoggingFormat(text); + } + /** + * Generate logging format from json key pairs + */ + public static fromJson(jsonLoggingFormat :{[key:string]: string}): LoggingFormat { + if (Object.keys(jsonLoggingFormat).length == 0) { + throw new Error('Json key pairs cannot be empty.'); + } + + return new JsonLoggingFormat(jsonLoggingFormat); + }; + + /** + * Called when the Access Log Format is initialized. Can be used to enforce + * mutual exclusivity with future properties + */ + public abstract bind(): LoggingFormatConfig; +} + +/** + * Configuration for Json logging format + */ +class JsonLoggingFormat extends LoggingFormat { + /** + * Json pattern for the output logs + */ + private readonly json: Array; + constructor(json: {[key:string]: string}) { + super(); + this.json = Object.entries(json).map(([key, value]) => ({ key, value })); + } + + bind(): LoggingFormatConfig { + return { + formatConfig: { + json: this.json, + }, + }; + } +} + +class TextLoggingFormat extends LoggingFormat { + /** + * Json pattern for the output logs + */ + private readonly text: string; + constructor(text: string) { + super(); + this.text = text; + } + + public bind(): LoggingFormatConfig { + return { + formatConfig: { + text: this.text, + }, + }; + } +} + /** * Represents the properties needed to define backend defaults */ diff --git a/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts b/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts index 8469daad9437c..b4446d3d1b5d0 100644 --- a/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts +++ b/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts @@ -109,6 +109,57 @@ describe('virtual gateway', () => { meshName: 'test-mesh', }); + new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + listeners: [appmesh.VirtualGatewayListener.grpc({ + port: 80, + healthCheck: appmesh.HealthCheck.grpc(), + })], + mesh: mesh, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 5000, + Port: 80, + Protocol: appmesh.Protocol.GRPC, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + }, + PortMapping: { + Port: 80, + Protocol: appmesh.Protocol.GRPC, + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + Format: { + Text: 'test_pattern', + }, + }, + }, + }, + }, + VirtualGatewayName: 'test-gateway', + }); + }); + test('without logging format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); new appmesh.VirtualGateway(stack, 'testGateway', { virtualGatewayName: 'test-gateway', listeners: [appmesh.VirtualGatewayListener.grpc({ @@ -149,7 +200,90 @@ describe('virtual gateway', () => { VirtualGatewayName: 'test-gateway', }); }); + test('with json logging format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + listeners: [appmesh.VirtualGatewayListener.grpc({ + port: 80, + healthCheck: appmesh.HealthCheck.grpc(), + })], + mesh: mesh, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', + appmesh.LoggingFormat.fromJson( + { testKey1: 'testValue1', testKey2: 'testValue2' })), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 5000, + Port: 80, + Protocol: appmesh.Protocol.GRPC, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + }, + PortMapping: { + Port: 80, + Protocol: appmesh.Protocol.GRPC, + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + Format: { + Json: [ + { + Key: 'testKey1', + Value: 'testValue1', + }, + { + Key: 'testKey2', + Value: 'testValue2', + }, + ], + }, + }, + }, + }, + }, + VirtualGatewayName: 'test-gateway', + }); + }); + test('test with invalid format input', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN and Then + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + expect(() => { + new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + listeners: [appmesh.VirtualGatewayListener.grpc({ + port: 80, + healthCheck: appmesh.HealthCheck.grpc(), + })], + mesh: mesh, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromJson({})), + }); + }).toThrow('Json key pairs cannot be empty.'); + }); test('with an http listener with a TLS certificate from ACM', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/aws-cdk-lib/aws-appmesh/test/virtual-node.test.ts b/packages/aws-cdk-lib/aws-appmesh/test/virtual-node.test.ts index f0aaac93c11d6..2eac9080e1f0f 100644 --- a/packages/aws-cdk-lib/aws-appmesh/test/virtual-node.test.ts +++ b/packages/aws-cdk-lib/aws-appmesh/test/virtual-node.test.ts @@ -90,7 +90,104 @@ describe('virtual node', () => { }); }); }); + describe('when file access logging is added', () => { + test('can add json format logging to the resource', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', + appmesh.LoggingFormat.fromJson( + { testKey1: 'testValue1', testKey2: 'testValue2' })), + }); + + node.addListener(appmesh.VirtualNodeListener.tcp({ + port: 8081, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8081, + Protocol: 'tcp', + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + Format: { + Json: [ + { + Key: 'testKey1', + Value: 'testValue1', + }, + { + Key: 'testKey2', + Value: 'testValue2', + }, + ], + }, + }, + }, + }, + }, + }); + }); + test('can add text format logging to the resource', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout', appmesh.LoggingFormat.fromText('test_pattern')), + }); + + node.addListener(appmesh.VirtualNodeListener.tcp({ + port: 8081, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8081, + Protocol: 'tcp', + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + Format: { + Text: 'test_pattern', + }, + }, + }, + }, + }, + }); + }); + }); describe('when a listener is added with timeout', () => { test('should add the listener timeout to the resource', () => { // GIVEN