Skip to content

Commit

Permalink
Merge pull request #1 from RomainMuller/api-gateway
Browse files Browse the repository at this point in the history
feat: support for watching API Gateway
  • Loading branch information
Elad Ben-Israel committed Jul 8, 2019
2 parents 106a025 + 717d800 commit ad6eb27
Show file tree
Hide file tree
Showing 7 changed files with 666 additions and 460 deletions.
23 changes: 5 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Watchful } from 'cdk-watchful'
const wf = new Watchful(this, 'watchful');
wf.watchDynamoTable('My Cute Little Table', myTable);
wf.watchLambdaFunction('My Function', myFunction);
wf.watchApiGateway('My REST API', myRestApi);
```

**Python:**
Expand All @@ -27,6 +28,7 @@ from cdk_watchful import Watchful
wf = Watchful(self, 'watchful')
wf.watch_dynamo_table('My Cute Little Table', my_table)
wf.watch_lambda_function('My Function', my_function)
wf.watch_api_gateway('My REST API', my_rest_api)
```

And...
Expand Down Expand Up @@ -74,26 +76,11 @@ wf = Watchful(self, 'watchful', alarm_email='your@amil.com')

Watchful manages a central dashboard and configures default alarming for:

- Amazon DynamoDB
- AWS Lambda
- Amazon DynamoDB: `watchful.watchDynamoTable`
- AWS Lambda: `watchful.watchLambdaFunction`
- Amazon API Gateway: `watchful.watchApiGateway`
- [Request yours](https://github.com/eladb/cdk-watchful/issues/new)

**TypeScript:**

```ts
wf.watchDynamoTable('My Happy Little Table', littleTable);
wf.watchDynamoTable('My Very Happy Table', veryHappyTable);
wf.watchLambdaFunction('The Function', fn);
```

**Python:**

```python
wf.watch_dynamo_table('My Happy Little Table', table)
wf.watch_lambda_function('Handler1', handler1)
wf.watch_lambda_function('Handler2', handler2)
```

## Watching Scopes

Watchful can also watch complete CDK construct scopes. It will automatically
Expand Down
176 changes: 176 additions & 0 deletions lib/api-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import apigw = require('@aws-cdk/aws-apigateway');
import { Metric, MetricOptions, ComparisonOperator, GraphWidget, HorizontalAnnotation } from '@aws-cdk/aws-cloudwatch';
import { Construct, Duration } from '@aws-cdk/core';
import { IWatchful } from './api';

export interface WatchApiGatewayOptions {
/**
* Alarm when 5XX errors reach this threshold over 5 minutes.
*
* @default 1 any 5xx HTTP response will trigger the alarm
*/
readonly serverErrorThreshold?: number;

/**
* A list of operations to monitor separately.
*
* @default - only API-level monitoring is added.
*/
readonly watchedOperations?: WatchedOperation[];

/**
* Include a dashboard graph for caching metrics
*
* @default false
*/
readonly cacheGraph?: boolean;
}

export interface WatchApiGatewayProps extends WatchApiGatewayOptions {
/**
* The title of this section.
*/
readonly title: string;

/**
* The Watchful instance to add widgets into.
*/
readonly watchful: IWatchful;

/**
* The API Gateway REST API that is being watched.
*/
readonly restApi: apigw.RestApi;
}

export class WatchApiGateway extends Construct {
private readonly api: apigw.CfnRestApi;
private readonly stage: string;
private readonly watchful: IWatchful;

constructor(scope: Construct, id: string, props: WatchApiGatewayProps) {
super(scope, id);

this.api = props.restApi.node.findChild('Resource') as apigw.CfnRestApi;
this.stage = props.restApi.deploymentStage.stageName;
this.watchful = props.watchful;

const alarmThreshold = props.serverErrorThreshold == null ? 1 : props.serverErrorThreshold;
if (alarmThreshold) {
this.watchful.addAlarm(
this.createApiGatewayMetric(ApiGatewayMetric.FiveHundredError)
.createAlarm(this, '5XXErrorAlarm', {
alarmDescription: `at ${alarmThreshold}`,
threshold: alarmThreshold,
period: Duration.minutes(5),
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
evaluationPeriods: 1,
statistic: 'sum',
})
);
}

this.watchful.addSection(props.title, {
links: [{ title: 'Amazon API Gateway Console', url: linkForApiGateway(props.restApi) }]
});
[undefined, ...props.watchedOperations || []].forEach(operation =>
this.watchful.addWidgets(
this.createCallGraphWidget(operation, alarmThreshold),
...props.cacheGraph ? [this.createCacheGraphWidget(operation)] : [],
this.createLatencyGraphWidget(ApiGatewayMetric.Latency, operation),
this.createLatencyGraphWidget(ApiGatewayMetric.IntegrationLatency, operation),
)
);
}

private createCallGraphWidget(opts?: WatchedOperation, alarmThreshold?: number) {
const leftAnnotations: HorizontalAnnotation[] = alarmThreshold
? [{ value: alarmThreshold, color: '#ff0000', label: '5XX Errors Alarm' }]
: [];

return new GraphWidget({
title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} Calls/min`,
width: 12,
stacked: false,
left: [
this.createApiGatewayMetric(ApiGatewayMetric.Count, opts, { label: 'Calls', statistic: 'sum', color: '#1f77b4' }),
this.createApiGatewayMetric(ApiGatewayMetric.FourHundredError, opts, { label: 'HTTP 4XX', statistic: 'sum', color: '#ff7f0e' }),
this.createApiGatewayMetric(ApiGatewayMetric.FiveHundredError, opts, { label: 'HTTP 5XX', statistic: 'sum', color: '#d62728' }),
],
leftAnnotations
});
}

private createCacheGraphWidget(opts?: WatchedOperation) {
return new GraphWidget({
title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} Cache/min`,
width: 12,
stacked: false,
left: [
this.createApiGatewayMetric(ApiGatewayMetric.Count, opts, { label: 'Calls', statistic: 'sum', color: '#1f77b4' }),
this.createApiGatewayMetric(ApiGatewayMetric.CacheHitCount, opts, { label: 'Cache Hit', statistic: 'sum', color: '#2ca02c' }),
this.createApiGatewayMetric(ApiGatewayMetric.CacheMissCount, opts, { label: 'Cache Miss', statistic: 'sum', color: '#d62728' }),
],
});
}

private createLatencyGraphWidget(metric: ApiGatewayMetric, opts?: WatchedOperation) {
return new GraphWidget({
title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} ${metric} (1-minute periods)`,
width: 12,
stacked: false,
left: ['min', 'avg', 'p90', 'p99', 'max'].map(statistic =>
this.createApiGatewayMetric(metric, opts, { label: statistic, statistic })),
});
}

private createApiGatewayMetric(
metricName: ApiGatewayMetric,
opts?: WatchedOperation,
metricOpts?: MetricOptions
): Metric {
return new Metric({
dimensions: {
ApiName: this.api.name,
Stage: this.stage,
...opts && {
Method: opts.httpMethod,
Resource: opts.resourcePath,
},
},
metricName,
namespace: 'AWS/ApiGateway',
period: Duration.minutes(1),
...metricOpts,
});
}
}

/**
* An operation (path and method) worth monitoring.
*/
export interface WatchedOperation {
/**
* The HTTP method for the operation (GET, POST, ...)
*/
readonly httpMethod: string;

/**
* The REST API path for this operation (/, /resource/{id}, ...)
*/
readonly resourcePath: string;
}

const enum ApiGatewayMetric {
FourHundredError = '4XXError',
FiveHundredError = '5XXError',
CacheHitCount = 'CacheHitCount',
CacheMissCount = 'CacheMissCount',
Count = 'Count',
IntegrationLatency = 'IntegrationLatency',
Latency = 'Latency',
}

function linkForApiGateway(api: apigw.IRestApi) {
return `https://console.aws.amazon.com/apigateway/home?region=${api.stack.region}#/apis/${api.restApiId}`;
}
12 changes: 12 additions & 0 deletions lib/aspect.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { IAspect, IConstruct } from '@aws-cdk/core';
import apigw = require('@aws-cdk/aws-apigateway');
import dynamodb = require('@aws-cdk/aws-dynamodb');
import lambda = require('@aws-cdk/aws-lambda');

export interface WatchfulAspectProps {
/**
* Automatically watch API Gateway APIs in the scope.
* @default true
*/
readonly apiGateway?: boolean;

/**
* Automatically watch all Amazon DynamoDB tables in the scope.
* @default true
Expand Down Expand Up @@ -30,9 +37,14 @@ export class WatchfulAspect implements IAspect {
}

public visit(node: IConstruct): void {
const watchApiGateway = this.props.apiGateway === undefined ? true : this.props.apiGateway;
const watchDynamo = this.props.dynamodb === undefined ? true : this.props.dynamodb;
const watchLambda = this.props.lambda === undefined ? true : this.props.lambda;

if (watchApiGateway && node instanceof apigw.RestApi) {
this.watchful.watchApiGateway(node.node.path, node);
}

if (watchDynamo && node instanceof dynamodb.Table) {
this.watchful.watchDynamoTable(node.node.path, node);
}
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './api';
export * from './watchful';
export * from './aspect';

export * from './api-gateway';
export * from './dynamodb';
export * from './lambda';
8 changes: 8 additions & 0 deletions lib/watchful.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Construct, CfnOutput } from '@aws-cdk/core';
import apigw = require('@aws-cdk/aws-apigateway');
import sns = require('@aws-cdk/aws-sns');
import sns_subscriptions = require('@aws-cdk/aws-sns-subscriptions');
import lambda = require('@aws-cdk/aws-lambda');
Expand All @@ -9,6 +10,7 @@ import { WatchDynamoTableOptions, WatchDynamoTable } from './dynamodb';
import { IWatchful, SectionOptions } from './api';
import { WatchLambdaFunctionOptions, WatchLambdaFunction } from './lambda';
import { WatchfulAspect, WatchfulAspectProps } from './aspect';
import { WatchApiGatewayOptions, WatchApiGateway } from './api-gateway';


export interface WatchfulProps {
Expand Down Expand Up @@ -67,6 +69,12 @@ export class Watchful extends Construct implements IWatchful {
});
}

public watchApiGateway(title: string, restApi: apigw.RestApi, options: WatchApiGatewayOptions = {}) {
return new WatchApiGateway(this, restApi.node.uniqueId, {
title, watchful: this, restApi, ...options
});
}

public watchLambdaFunction(title: string, fn: lambda.Function, options: WatchLambdaFunctionOptions = {}) {
return new WatchLambdaFunction(this, fn.node.uniqueId, {
title, watchful: this, fn, ...options
Expand Down
Loading

0 comments on commit ad6eb27

Please sign in to comment.