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(apigatewayv2): http api - metric methods for api and stage #10686

Merged
merged 25 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors)
- [Publishing HTTP APIs](#publishing-http-apis)
- [Custom Domain](#custom-domain)
- [Metrics](#metrics)

## Introduction

Expand Down Expand Up @@ -198,3 +199,22 @@ with 3 API mapping resources across different APIs and Stages.
| api | $default | `https://${domainName}/foo` |
| api | beta | `https://${domainName}/bar` |
| apiDemo | $default | `https://${domainName}/demo` |

## Metrics

The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. These metrics can be referred to using the metric APIs available on the HttpApi construct. The APIs with the metric prefix can be used to get reference to specific metrics for this API. For example, the method below refers to the client side errors metric for this API.
cyuste marked this conversation as resolved.
Show resolved Hide resolved

```
const api = new apigw.HttpApi(stack, 'my-api');
const clientErrorMetric = api.metricClientError();

```
Please note that this will return a metric for all the stages defined in the api. It is also possible to collect metrics only from a specic `stage` using the metric methods from the `stage`.
cyuste marked this conversation as resolved.
Show resolved Hide resolved

```
const api = new apigw.HttpApi(stack, 'my-api');
const stage = new HttpStage(stack, 'Stage', {
httpApi: api,
});
const clientErrorMetric = stage.metricClientError();
```
128 changes: 112 additions & 16 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch';
import { Duration, IResource, Resource } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnApi, CfnApiProps } from '../apigatewayv2.generated';
Expand All @@ -16,10 +17,67 @@ export interface IHttpApi extends IResource {
*/
readonly httpApiId: string;

/**
* A human friendly name for this HTTP API. Note that this is different from `httpApiId`.
*/
readonly httpApiName?: string;
nija-at marked this conversation as resolved.
Show resolved Hide resolved

/**
* The default stage
*/
readonly defaultStage?: HttpStage;

/**
* Return the given named metric for this HTTP Api Gateway
*
* @default Average over 5 minutes
cyuste marked this conversation as resolved.
Show resolved Hide resolved
*/
metric(metricName: string, props?: MetricOptions): Metric;

/**
* Metric for the number of client-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
metricClientError(props?: MetricOptions): Metric;

/**
* Metric for the number of server-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
metricServerError(props?: MetricOptions): Metric;

/**
* Metric for the amount of data processed in bytes.
*
* @default - sum over 5 minutes
*/
metricDataProcessed(props?: MetricOptions): Metric;

/**
* Metric for the total number API requests in a given period.
*
* @default - SampleCount over 5 minutes
*/
metricCount(props?: MetricOptions): Metric;

/**
* Metric for the time between when API Gateway relays a request to the backend
* and when it receives a response from the backend.
*
* @default - no statistic
*/
metricIntegrationLatency(props?: MetricOptions): Metric;

/**
* The time between when API Gateway receives a request from a client
* and when it returns a response to the client.
* The latency includes the integration latency and other API Gateway overhead.
*
* @default - no statistic
*/
metricLatency(props?: MetricOptions): Metric;
}

/**
Expand Down Expand Up @@ -116,22 +174,11 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions {
readonly methods?: HttpMethod[];
}

/**
* Create a new API Gateway HTTP API endpoint.
* @resource AWS::ApiGatewayV2::Api
*/
export class HttpApi extends Resource implements IHttpApi {
/**
* Import an existing HTTP API into this CDK app.
*/
public static fromApiId(scope: Construct, id: string, httpApiId: string): IHttpApi {
class Import extends Resource implements IHttpApi {
public readonly httpApiId = httpApiId;
}
return new Import(scope, id);
}
class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported

public readonly httpApiId: string;
public readonly httpApiName?: string;

/**
* default stage of the api resource
*/
Expand All @@ -140,7 +187,7 @@ export class HttpApi extends Resource implements IHttpApi {
constructor(scope: Construct, id: string, props?: HttpApiProps) {
super(scope, id);

const apiName = props?.apiName ?? id;
this.httpApiName = props?.apiName ?? id;

nija-at marked this conversation as resolved.
Show resolved Hide resolved
let corsConfiguration: CfnApi.CorsProperty | undefined;
if (props?.corsPreflight) {
Expand All @@ -167,7 +214,7 @@ export class HttpApi extends Resource implements IHttpApi {
}

const apiProps: CfnApiProps = {
name: apiName,
name: this.httpApiName,
protocolType: 'HTTP',
corsConfiguration,
};
Expand Down Expand Up @@ -202,6 +249,55 @@ export class HttpApi extends Resource implements IHttpApi {
}
}

public metric(metricName: string, props?: MetricOptions): Metric {
return new Metric({
namespace: 'AWS/ApiGateway',
metricName,
dimensions: { ApiId: this.httpApiId },
...props,
}).attachTo(this);
}

public metricClientError(props?: MetricOptions): Metric {
return this.metric('4XXError', { statistic: 'Sum', ...props });
}

public metricServerError(props?: MetricOptions): Metric {
return this.metric('5XXError', { statistic: 'Sum', ...props });
}

public metricDataProcessed(props?: MetricOptions): Metric {
return this.metric('DataProcessed', { statistic: 'Sum', ...props });
}

public metricCount(props?: MetricOptions): Metric {
return this.metric('Count', { statistic: 'SampleCount', ...props });
}

public metricIntegrationLatency(props?: MetricOptions): Metric {
return this.metric('IntegrationLatency', props);
}

public metricLatency(props?: MetricOptions): Metric {
return this.metric('Latency', props);
}
}

/**
* Create a new API Gateway HTTP API endpoint.
* @resource AWS::ApiGatewayV2::Api
*/
export class HttpApi extends HttpApiBase {
/**
* Import an existing HTTP API into this CDK app.
*/
public static fromApiId(scope: Construct, id: string, httpApiId: string): IHttpApi {
class Import extends HttpApiBase {
public readonly httpApiId = httpApiId;
}
return new Import(scope, id);
}

/**
* Get the URL to the default stage of this API.
* Returns `undefined` if `createDefaultStage` is unset.
Expand Down
71 changes: 71 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch';
import { Resource, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnStage } from '../apigatewayv2.generated';
import { CommonStageOptions, IDomainName, IStage } from '../common';
import { IHttpApi } from './api';
import { HttpApiMapping } from './api-mapping';


const DEFAULT_STAGE_NAME = '$default';

/**
Expand Down Expand Up @@ -117,4 +119,73 @@ export class HttpStage extends Resource implements IStage {
const urlPath = this.stageName === DEFAULT_STAGE_NAME ? '' : this.stageName;
return `https://${this.httpApi.httpApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`;
}

/**
* Return the given named metric for this HTTP Api Gateway Stage
*
* @default Average over 5 minutes
cyuste marked this conversation as resolved.
Show resolved Hide resolved
*/
public metric(metricName: string, props?: MetricOptions): Metric {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could apply the same logic as HttpApi of moving this into the interface and creating an abstract HttpStageBase class.

This is entirely optional if you want to skip it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you prefer. Currently I can import an api and get its metrics (created a new test for it) but I can't for a stage, what is correct as I cannot either associate an imported stage to an api and therefore cannot set the metric dimensions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine. It can be done later as a separate PR.

var api = this.httpApi;
return api.metric(metricName, props).with({
dimensions: { ApiId: this.httpApi.httpApiId, Stage: this.stageName },
}).attachTo(this);
}

/**
* Metric for the number of client-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
public metricClientError(props?: MetricOptions): Metric {
return this.metric('4XXError', { statistic: 'Sum', ...props });
}

/**
* Metric for the number of server-side errors captured in a given period.
*
* @default - sum over 5 minutes
*/
public metricServerError(props?: MetricOptions): Metric {
return this.metric('5XXError', { statistic: 'Sum', ...props });
}

/**
* Metric for the amount of data processed in bytes.
*
* @default - sum over 5 minutes
*/
public metricDataProcessed(props?: MetricOptions): Metric {
return this.metric('DataProcessed', { statistic: 'Sum', ...props });
}

/**
* Metric for the total number API requests in a given period.
*
* @default - SampleCount over 5 minutes
*/
public metricCount(props?: MetricOptions): Metric {
return this.metric('Count', { statistic: 'SampleCount', ...props });
}

/**
* Metric for the time between when API Gateway relays a request to the backend
* and when it receives a response from the backend.
*
* @default - no statistic
*/
public metricIntegrationLatency(props?: MetricOptions): Metric {
return this.metric('IntegrationLatency', props);
}

/**
* The time between when API Gateway receives a request from a client
* and when it returns a response to the client.
* The latency includes the integration latency and other API Gateway overhead.
*
* @default - no statistic
*/
public metricLatency(props?: MetricOptions): Metric {
return this.metric('Latency', props);
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.0.4"
},
"peerDependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.0.4"
},
Expand Down
46 changes: 46 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@aws-cdk/assert/jest';
import { ABSENT } from '@aws-cdk/assert';
import { Metric } from '@aws-cdk/aws-cloudwatch';
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';
import { Duration, Stack } from '@aws-cdk/core';
import { HttpApi, HttpMethod, LambdaProxyIntegration } from '../../lib';
Expand Down Expand Up @@ -155,5 +156,50 @@ describe('HttpApi', () => {
},
})).toThrowError(/allowCredentials is not supported/);
});

test('get metric', () => {
// GIVEN
const stack = new Stack();
const api = new HttpApi(stack, 'test-api', {
createDefaultStage: false,
});
const metricName = '4xxError';
const statistic = 'Sum';
const apiId = api.httpApiId;

// WHEN
const countMetric = api.metric(metricName, { statistic });

// THEN
expect(countMetric.namespace).toEqual('AWS/ApiGateway');
expect(countMetric.metricName).toEqual(metricName);
expect(countMetric.dimensions).toEqual({ ApiId: apiId });
expect(countMetric.statistic).toEqual(statistic);
});

test('Exercise metrics', () => {
// GIVEN
const stack = new Stack();
const api = new HttpApi(stack, 'test-api', {
createDefaultStage: false,
});
const color = '#00ff00';
const apiId = api.httpApiId;

// WHEN
const metrics = new Array<Metric>();
metrics.push(api.metricClientError({ color }));
metrics.push(api.metricServerError({ color }));
metrics.push(api.metricDataProcessed({ color }));
metrics.push(api.metricLatency({ color }));
metrics.push(api.metricIntegrationLatency({ color }));
metrics.push(api.metricCount({ color }));
// THEN
for (const metric of metrics) {
expect(metric.namespace).toEqual('AWS/ApiGateway');
expect(metric.dimensions).toEqual({ ApiId: apiId });
expect(metric.color).toEqual(color);
}
});
});
});
Loading