diff --git a/packages/aws-cdk-lib/aws-appsync/README.md b/packages/aws-cdk-lib/aws-appsync/README.md index a239a481e057a..4d9cc3cd5938e 100644 --- a/packages/aws-cdk-lib/aws-appsync/README.md +++ b/packages/aws-cdk-lib/aws-appsync/README.md @@ -562,6 +562,50 @@ new appsync.GraphqlApi(this, 'api', { }); ``` +## Enhanced Monitoring + +AppSync by default enables certain monitoring with AWS CloudWatch. However, you can add enhanced monitoring to get more details about incoming requests on more granular level. +The configuration allows you to specify, whether you wish to collect metrics on data source, operation or resolver level. + +```ts +new appsync.GraphqlApi(this, 'api', { + authorizationConfig: {}, + name: 'myApi', + definition: appsync.Definition.fromFile(path.join(__dirname, 'myApi.graphql')), + enhancedMonitoringConfig: { + dataSourceLevelMetricsBehavior: appsync.DataSourceLevelMetricsBehavior.FULL_REQUEST_DATA_SOURCE_METRICS, + operationLevelMetricsConfig: appsync.OperationLevelMetricsConfig.ENABLED, + resolverLevelMetricsBehavior: appsync.ResolverLevelMetricsBehavior.FULL_REQUEST_RESOLVER_METRICS + }, +}); +``` + +If you wish to enable enhanced monitoring only for subset of data sources or resolvers you are use following configuration: + +```ts +const api = new appsync.GraphqlApi(this, 'api', { + authorizationConfig: {}, + name: 'myApi', + definition: appsync.Definition.fromFile(path.join(__dirname, 'myApi.graphql')), + enhancedMonitoringConfig: { + dataSourceLevelMetricsBehavior: appsync.DataSourceLevelMetricsBehavior.PER_DATA_SOURCE_METRICS, + operationLevelMetricsConfig: appsync.OperationLevelMetricsConfig.ENABLED, + resolverLevelMetricsBehavior: appsync.ResolverLevelMetricsBehavior.PER_RESOLVER_METRICS + }, +}); + +const noneDS = api.addNoneDataSource('none', { + metricsConfig: appsync.MetricsConfig.ENABLED, +}); + +noneDS.createResolver('noneResolver', { + typeName: 'Mutation', + fieldName: 'addDemoMetricsConfig', + metricsConfig: appsync.MetricsConfig.ENABLED +}); + +``` + ## Schema You can define a schema using from a local file using `Definition.fromFile` diff --git a/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts b/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts index 44c6583268ac0..08bf71c0cfabd 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { BaseAppsyncFunctionProps, AppsyncFunction } from './appsync-function'; import { CfnDataSource } from './appsync.generated'; -import { IGraphqlApi } from './graphqlapi-base'; +import { IGraphqlApi, MetricsConfig } from './graphqlapi-base'; import { BaseResolverProps, Resolver } from './resolver'; import { ITable } from '../../aws-dynamodb'; import { IDomain as IElasticsearchDomain } from '../../aws-elasticsearch'; @@ -33,6 +33,12 @@ export interface BaseDataSourceProps { * @default - None */ readonly description?: string; + /** + * metrics config + * + * @default - No config + */ + readonly metricsConfig?: MetricsConfig; } /** @@ -131,6 +137,7 @@ export abstract class BaseDataSource extends Construct { apiId: props.api.apiId, name: supportedName, description: props.description, + metricsConfig: props.metricsConfig, serviceRoleArn: this.serviceRole?.roleArn, ...extended, }); diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts index 7b87718a52e5d..e9843508c4ffa 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts @@ -20,6 +20,21 @@ import { IDatabaseCluster, IServerlessCluster } from '../../aws-rds'; import { ISecret } from '../../aws-secretsmanager'; import { ArnFormat, CfnResource, IResource, Resource, Stack } from '../../core'; +/** + * Defines, whether certain entity should use Enhanced Metrics + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-enhancedmetricsconfig.html#aws-properties-appsync-graphqlapi-enhancedmetricsconfig-properties + */ +export enum MetricsConfig { + /** + * Disable Enhanced Metrics for Data Source or Resolver + */ + DISABLED = 'DISABLED', + /** + * Enable Enhanced Metrics for Data Source or Resolver + */ + ENABLED = 'ENABLED', +} + /** * Optional configuration for data sources */ @@ -37,6 +52,14 @@ export interface DataSourceOptions { * @default - No description */ readonly description?: string; + + /** + * Metrics config, which defines whether this data source will use Enhanced Metrics or not. + * Value will be ignored, if `dataSourceLevelMetricsBehavior` on AppSync construct is set to `FULL_REQUEST_DATA_SOURCE_METRICS`. + * + * @default - none + */ + readonly metricsConfig?: MetricsConfig; } /** @@ -378,6 +401,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { api: this, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, }); } @@ -394,6 +418,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { table, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, }); } @@ -411,6 +436,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { name: options?.name, description: options?.description, authorizationConfig: options?.authorizationConfig, + metricsConfig: options?.metricsConfig, }); } @@ -427,6 +453,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { lambdaFunction, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, }); } @@ -449,6 +476,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { api: this, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, serverlessCluster, secretStore, databaseName, @@ -474,6 +502,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { api: this, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, serverlessCluster, secretStore, databaseName, @@ -493,6 +522,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { api: this, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, domain, }); } @@ -509,6 +539,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { eventBus, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, }); } @@ -524,6 +555,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { api: this, name: options?.name, description: options?.description, + metricsConfig: options?.metricsConfig, domain, }); } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts index 4332257d5484c..5fb839260419d 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts @@ -234,6 +234,70 @@ export interface LogConfig { readonly retention?: RetentionDays; } +/** + * Defines the way data source enhanced metrics will behave. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-enhancedmetricsconfig.html#aws-properties-appsync-graphqlapi-enhancedmetricsconfig-properties + */ +export enum DataSourceLevelMetricsBehavior { + /** + * Records and emits metric data for all data sources in the request. + */ + FULL_REQUEST_DATA_SOURCE_METRICS = 'FULL_REQUEST_DATA_SOURCE_METRICS', + /** + * Records and emits metric data for data sources that have the `metricsConfig` value set to `MetricsConfig.ENABLED`. + */ + PER_DATA_SOURCE_METRICS = 'PER_DATA_SOURCE_METRICS', +} + +/** + * Defines the way data source enhanced metrics will behave. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-enhancedmetricsconfig.html#aws-properties-appsync-graphqlapi-enhancedmetricsconfig-properties + */ +export enum ResolverLevelMetricsBehavior { + /** + * Records and emits metric data for all resolvers in the request. + */ + FULL_REQUEST_RESOLVER_METRICS = 'FULL_REQUEST_RESOLVER_METRICS', + /** + * Records and emits metric data for resolvers that have the `metricsConfig` value set to `MetricsConfig.ENABLED`. + */ + PER_RESOLVER_METRICS = 'PER_RESOLVER_METRICS', +} + +/** + * Defines, whether Operation Level Metrics are enabled + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-enhancedmetricsconfig.html#aws-properties-appsync-graphqlapi-enhancedmetricsconfig-properties + */ +export enum OperationLevelMetricsConfig { + /** + * Disabled Operation Level Metrics + */ + DISABLED = 'DISABLED', + /** + * Enable Operation Level Metrics + */ + ENABLED = 'ENABLED', +} + +/** + * Enhanced Monitoring configuration for AppSync + * @see https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#cw-metrics + */ +export interface EnhancedMonitoringConfig { + /** + * Controls how data source metrics will be emitted to CloudWatch. + */ + readonly dataSourceLevelMetricsBehavior: DataSourceLevelMetricsBehavior; + /** + * Controls how operation metrics will be emitted to CloudWatch. + */ + readonly operationLevelMetricsConfig: OperationLevelMetricsConfig; + /** + * Controls how resolver metrics will be emitted to CloudWatch. + */ + readonly resolverLevelMetricsBehavior: ResolverLevelMetricsBehavior; +} + /** * Domain name configuration for AppSync */ @@ -431,6 +495,13 @@ export interface GraphqlApiProps { * @default - No environment variables. */ readonly environmentVariables?: { [key: string]: string }; + + /** + * Enhanced Monitoring Configuration for this AppSync API + * + * @default - None + */ + readonly enhancedMonitoringConfig?: EnhancedMonitoringConfig; } /** @@ -645,6 +716,7 @@ export class GraphqlApi extends GraphqlApiBase { queryDepthLimit: props.queryDepthLimit, resolverCountLimit: props.resolverCountLimit, environmentVariables: Lazy.any({ produce: () => this.renderEnvironmentVariables() }), + enhancedMetricsConfig: props.enhancedMonitoringConfig, }); this.apiId = this.api.attrApiId; diff --git a/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts b/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts index 6fc311aef634a..f8a281a53fe1c 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts @@ -5,7 +5,7 @@ import { CachingConfig } from './caching-config'; import { BASE_CACHING_KEYS } from './caching-key'; import { Code } from './code'; import { BaseDataSource } from './data-source'; -import { IGraphqlApi } from './graphqlapi-base'; +import { IGraphqlApi, MetricsConfig } from './graphqlapi-base'; import { MappingTemplate } from './mapping-template'; import { FunctionRuntime } from './runtime'; import { Token } from '../../core'; @@ -66,6 +66,12 @@ export interface BaseResolverProps { * @default - no code is used */ readonly code?: Code; + /** + * Metrics Config + * + * @default - none + */ + readonly metricsConfig?: MetricsConfig; } /** @@ -147,6 +153,7 @@ export class Resolver extends Construct { responseMappingTemplate: props.responseMappingTemplate ? props.responseMappingTemplate.renderTemplate() : undefined, cachingConfig: this.createCachingConfig(props.cachingConfig), maxBatchSize: props.maxBatchSize, + metricsConfig: props.metricsConfig, }); props.api.addSchemaDependency(this.resolver); if (props.dataSource) { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-dynamodb.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-dynamodb.test.ts index 1b64a62e04732..588de5e87b61b 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-dynamodb.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-dynamodb.test.ts @@ -71,6 +71,19 @@ describe('DynamoDb Data Source configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addDynamoDbDataSource('ds', table, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AMAZON_DYNAMODB', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple dynamo db data sources with no configuration', () => { // THEN expect(() => { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-elasticsearch.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-elasticsearch.test.ts index af624325fea3d..1cac298837988 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-elasticsearch.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-elasticsearch.test.ts @@ -109,6 +109,19 @@ describeDeprecated('Appsync Elasticsearch integration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addElasticsearchDataSource('ds', domain, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AMAZON_ELASTICSEARCH', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple elasticsearch data sources with no configuration', () => { // WHEN const when = () => { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-eventbridge.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-eventbridge.test.ts index de49a2219a50f..c5b775ca843f1 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-eventbridge.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-eventbridge.test.ts @@ -76,6 +76,17 @@ describe('EventBridge Data Source Configuration', () => { }); }); + test('A custom metrics config is used when provided', () => { + // WHEN + api.addEventBridgeDataSource('id', eventBus, { metricsConfig: appsync.MetricsConfig.ENABLED }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AMAZON_EVENTBRIDGE', + MetricsConfig: 'ENABLED', + }); + }); + test('A custom description is used when provided', () => { // WHEN api.addEventBridgeDataSource('ds', eventBus, { name: 'custom', description: 'custom description' }); diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-http.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-http.test.ts index 5e81aabe14749..0c7e016bd2f23 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-http.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-http.test.ts @@ -58,6 +58,19 @@ describe('Http Data Source configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addHttpDataSource('ds', endpoint, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'HTTP', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync configures name, authorizationConfig correctly', () => { // WHEN api.addHttpDataSource('ds', endpoint, { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-lambda.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-lambda.test.ts index e709e445b2d98..7f9839795e7fc 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-lambda.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-lambda.test.ts @@ -65,6 +65,19 @@ describe('Lambda Data Source configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addLambdaDataSource('ds', func, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync sanitized datasource name from unsupported characters', () => { const badCharacters = [...'!@#$%^&*()+-=[]{}\\|;:\'",<>?/']; diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-none.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-none.test.ts index da16eb8825f23..41573fccf18ed 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-none.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-none.test.ts @@ -83,6 +83,19 @@ describe('None Data Source configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addNoneDataSource('ds', { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'NONE', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple none data sources with no configuration', () => { // THEN expect(() => { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-opensearch.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-opensearch.test.ts index e2b77bc9acecf..b378020f3579a 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-opensearch.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-opensearch.test.ts @@ -106,6 +106,19 @@ describe('OpenSearch Data Source Configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addOpenSearchDataSource('ds', domain, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AMAZON_OPENSEARCH_SERVICE', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple openSearch data sources with no configuration', () => { // WHEN const when = () => { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-rds.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-rds.test.ts index 0f8e3a1790a04..9957f8719de73 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync-rds.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-rds.test.ts @@ -230,6 +230,19 @@ describe('Rds Data Source configuration', () => { }); }); + test('appsync configures metrics config correctly', () => { + // WHEN + api.addRdsDataSource('ds', serverlessCluster, secret, undefined, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple rds data sources with no configuration', () => { // WHEN const when = () => { @@ -467,6 +480,19 @@ describe('Rds Data Source Serverless V2 configuration', () => { }); }); + test('appsync configures metrics config correctly serverlessV2', () => { + // WHEN + api.addRdsDataSourceV2('dsV2', serverlessClusterV2, secret, undefined, { + metricsConfig: appsync.MetricsConfig.ENABLED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + MetricsConfig: 'ENABLED', + }); + }); + test('appsync errors when creating multiple rds data sources with no configuration ServerlessV2', () => { // WHEN const when = () => { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync.test.ts index 015491c97e891..11bfc7ff0ffe6 100644 --- a/packages/aws-cdk-lib/aws-appsync/test/appsync.test.ts +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync.test.ts @@ -370,4 +370,27 @@ test('when resolver limit is out of range, it throws an error', () => { expect(() => buildWithLimit('resolver-limit-max', 10000)).not.toThrow(errorString); expect(() => buildWithLimit('resolver-limit-high', 10001)).toThrow(errorString); +}); + +test('when metrics config is set, they should be used on API', () => { + // WHEN + new appsync.GraphqlApi(stack, 'metrics-config', { + authorizationConfig: {}, + name: 'metrics-config', + schema: appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql')), + enhancedMonitoringConfig: { + dataSourceLevelMetricsBehavior: appsync.DataSourceLevelMetricsBehavior.FULL_REQUEST_DATA_SOURCE_METRICS, + operationLevelMetricsConfig: appsync.OperationLevelMetricsConfig.ENABLED, + resolverLevelMetricsBehavior: appsync.ResolverLevelMetricsBehavior.FULL_REQUEST_RESOLVER_METRICS, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', { + EnhancedMetricsConfig: { + DataSourceLevelMetricsBehavior: 'FULL_REQUEST_DATA_SOURCE_METRICS', + OperationLevelMetricsConfig: 'ENABLED', + ResolverLevelMetricsBehavior: 'FULL_REQUEST_RESOLVER_METRICS', + }, + }); }); \ No newline at end of file