Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appsync): add Enhanced Monitoring #29940

Closed
44 changes: 44 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/README.md
Expand Up @@ -563,6 +563,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: DataSourceLevelMetricsBehavior.FULL_REQUEST_DATA_SOURCE_METRICS,
operationLevelMetricsConfig: OperationLevelMetricsConfig.ENABLED,
resolverLevelMetricsBehavior: 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: DataSourceLevelMetricsBehavior.PER_DATA_SOURCE_METRICS,
operationLevelMetricsConfig: OperationLevelMetricsConfig.ENABLED,
resolverLevelMetricsBehavior: ResolverLevelMetricsBehavior.PER_RESOLVER_METRICS
},
});

const noneDS = api.addNoneDataSource('none', {
metricsConfig: MetricsConfig.ENABLED,
});

noneDS.createResolver('noneResolver', {
typeName: 'Mutation',
fieldName: 'addDemoMetricsConfig',
metricsConfig: MetricsConfig.ENABLED
});

```

## Schema

You can define a schema using from a local file using `Definition.fromFile`
Expand Down
9 changes: 8 additions & 1 deletion 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';
Expand Down Expand Up @@ -33,6 +33,12 @@ export interface BaseDataSourceProps {
* @default - None
*/
readonly description?: string;
/**
* metrics config
*
* @default - No config
*/
readonly metricsConfig?: MetricsConfig;
}

/**
Expand Down Expand Up @@ -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,
});
Expand Down
32 changes: 32 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts
Expand Up @@ -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
*/
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -307,6 +330,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
api: this,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
});
}

Expand All @@ -323,6 +347,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
table,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
});
}

Expand All @@ -340,6 +365,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
name: options?.name,
description: options?.description,
authorizationConfig: options?.authorizationConfig,
metricsConfig: options?.metricsConfig,
});
}

Expand All @@ -356,6 +382,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
lambdaFunction,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
});
}

Expand All @@ -378,6 +405,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
api: this,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
serverlessCluster,
secretStore,
databaseName,
Expand All @@ -403,6 +431,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
api: this,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
serverlessCluster,
secretStore,
databaseName,
Expand All @@ -422,6 +451,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
api: this,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
domain,
});
}
Expand All @@ -438,6 +468,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
eventBus,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
});
}

Expand All @@ -453,6 +484,7 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi {
api: this,
name: options?.name,
description: options?.description,
metricsConfig: options?.metricsConfig,
domain,
});
}
Expand Down
72 changes: 72 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts
Expand Up @@ -260,6 +260,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;
}

/**
* Visibility type for a GraphQL API
*/
Expand Down Expand Up @@ -472,6 +536,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;
}

/**
Expand Down Expand Up @@ -646,6 +717,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;
Expand Down
9 changes: 8 additions & 1 deletion packages/aws-cdk-lib/aws-appsync/lib/resolver.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -66,6 +66,12 @@ export interface BaseResolverProps {
* @default - no code is used
*/
readonly code?: Code;
/**
* Metrics Config
*
* @default - none
*/
readonly metricsConfig?: MetricsConfig;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/test/appsync-dynamodb.test.ts
Expand Up @@ -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(() => {
Expand Down
Expand Up @@ -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 = () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/test/appsync-eventbridge.test.ts
Expand Up @@ -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' });
Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk-lib/aws-appsync/test/appsync-http.test.ts
Expand Up @@ -58,6 +58,19 @@ describe('Http Data Source configuration', () => {
});
});

test('appsync configures name 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, {
Expand Down