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(cloudfront): additional cloudfront distribution metrics #28777

Merged
merged 21 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { TestOrigin } from './test-origin';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';

class DistributionMetricsTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// CloudFront distribution setup
const distribution = new cloudfront.Distribution(this, 'Dist', {
defaultBehavior: { origin: new TestOrigin('www.example.com') },
publishAdditionalMetrics: true,
});

// Utility function to create alarms
const createAlarm = (alarmName: string, metric: cloudwatch.Metric) => {
return new cloudwatch.Alarm(this, alarmName, {
evaluationPeriods: 1,
threshold: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
metric: metric,
});
};

createAlarm('Alarm1', distribution.metricOriginLatency());
createAlarm('Alarm2', distribution.metricCacheHitRate());
createAlarm('Alarm3', distribution.metric401ErrorRate());
createAlarm('Alarm4', distribution.metric403ErrorRate());
createAlarm('Alarm5', distribution.metric404ErrorRate());
createAlarm('Alarm6', distribution.metric502ErrorRate());
createAlarm('Alarm7', distribution.metric503ErrorRate());
createAlarm('Alarm8', distribution.metric504ErrorRate());
}
}

const app = new cdk.App();
const stack = new DistributionMetricsTestStack(app, 'MyTestStack');

new IntegTest(app, 'MyTest', {
testCases: [stack],
});
15 changes: 15 additions & 0 deletions packages/aws-cdk-lib/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,21 @@ new cloudfront.Distribution(this, 'myDist', {
});
```

### Additional CloudFront distribution metrics

You can enable [additional CloudFront distribution metrics](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html#monitoring-console.distributions-additional), which include the following metrics:

- 4xx and 5xx error rates: View 4xx and 5xx error rates by the specific HTTP status code, as a percentage of total requests.
- Origin latency: See the total time spent from when CloudFront receives a request to when it provides a response to the network (not the viewer), for responses that are served from the origin, not the CloudFront cache.
- Cache hit rate: View cache hits as a percentage of total cacheable requests, excluding errors.

```ts
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') },
publishAdditionalMetrics: true,
});
```

GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
### HTTP Versions

You can configure CloudFront to use a particular version of the HTTP protocol. By default,
Expand Down
154 changes: 153 additions & 1 deletion packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct } from 'constructs';
import { ICachePolicy } from './cache-policy';
import { CfnDistribution } from './cloudfront.generated';
import { CfnDistribution, CfnMonitoringSubscription } from './cloudfront.generated';
import { FunctionAssociation } from './function';
import { GeoRestriction } from './geo-restriction';
import { IKeyGroup } from './key-group';
Expand All @@ -11,6 +11,7 @@ import { formatDistributionArn } from './private/utils';
import { IRealtimeLogConfig } from './realtime-log-config';
import { IResponseHeadersPolicy } from './response-headers-policy';
import * as acm from '../../aws-certificatemanager';
import * as cloudwatch from '../../aws-cloudwatch';
import * as iam from '../../aws-iam';
import * as lambda from '../../aws-lambda';
import * as s3 from '../../aws-s3';
Expand Down Expand Up @@ -255,6 +256,15 @@ export interface DistributionProps {
* @default SSLMethod.SNI
*/
readonly sslSupportMethod?: SSLMethod;

/**
* Whether to enable additional CloudWatch metrics.
*
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html
*
* @default false
*/
readonly publishAdditionalMetrics?: boolean;
}

/**
Expand Down Expand Up @@ -298,6 +308,7 @@ export class Distribution extends Resource implements IDistribution {

private readonly errorResponses: ErrorResponse[];
private readonly certificate?: acm.ICertificate;
private readonly publishAdditionalMetrics?: boolean;

constructor(scope: Construct, id: string, props: DistributionProps) {
super(scope, id);
Expand All @@ -323,6 +334,7 @@ export class Distribution extends Resource implements IDistribution {

this.certificate = props.certificate;
this.errorResponses = props.errorResponses ?? [];
this.publishAdditionalMetrics = props.publishAdditionalMetrics;

// Comments have an undocumented limit of 128 characters
const trimmedComment =
Expand Down Expand Up @@ -355,6 +367,146 @@ export class Distribution extends Resource implements IDistribution {
this.domainName = distribution.attrDomainName;
this.distributionDomainName = distribution.attrDomainName;
this.distributionId = distribution.ref;

if (props.publishAdditionalMetrics) {
new CfnMonitoringSubscription(this, 'MonitoringSubscription', {
distributionId: this.distributionId,
monitoringSubscription: {
realtimeMetricsSubscriptionConfig: {
realtimeMetricsSubscriptionStatus: 'Enabled',
},
},
});
}
}

/**
* Return the given named metric for this Distribution
*/
public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return new cloudwatch.Metric({
namespace: 'AWS/CloudFront',
metricName,
dimensionsMap: { DistributionId: this.distributionId },
...props,
});
}

/**
* Metric for the total time spent from when CloudFront receives a request to when it starts providing a response to the network (not the viewer),
* for requests that are served from the origin, not the CloudFront cache.
*
* This is also known as first byte latency, or time-to-first-byte.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metricOriginLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('Origin latency metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('OriginLatency', props);
}

/**
* Metric for the percentage of all cacheable requests for which CloudFront served the content from its cache.
*
* HTTP POST and PUT requests, and errors, are not considered cacheable requests.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metricCacheHitRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('Cache hit rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('CacheHitRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 401.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric401ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('401 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('401ErrorRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 403.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric403ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('403 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('403ErrorRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 404.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric404ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('404 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('404ErrorRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 502.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric502ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('502 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('502ErrorRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 503.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric503ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('503 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('503ErrorRate', props);
}

/**
* Metric for the percentage of all viewer requests for which the response's HTTP status code is 504.
*
* To obtain this metric, you need to set `publishAdditionalMetrics` to `true`.
*
* @default - average over 5 minutes
*/
public metric504ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
if (this.publishAdditionalMetrics !== true) {
throw new Error('504 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'');
}
return this.metric('504ErrorRate', props);
}

/**
Expand Down
83 changes: 83 additions & 0 deletions packages/aws-cdk-lib/aws-cloudfront/test/distribution.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { defaultOrigin, defaultOriginGroup } from './test-origin';
import { Match, Template } from '../../assertions';
import * as acm from '../../aws-certificatemanager';
import * as cloudwatch from '../../aws-cloudwatch';
import * as iam from '../../aws-iam';
import * as kinesis from '../../aws-kinesis';
import * as lambda from '../../aws-lambda';
import * as s3 from '../../aws-s3';
import { App, Duration, Stack } from '../../core';
import { exec as _exec } from 'child_process';
import {
CfnDistribution,
Distribution,
Expand Down Expand Up @@ -1241,3 +1243,84 @@ test('render distribution behavior with realtime log config - multiple behaviors
},
}));
});

test('with publish additional metrics', () => {
const origin = defaultOrigin();
new Distribution(stack, 'MyDist', {
defaultBehavior: { origin },
publishAdditionalMetrics: true,
});

Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', {
DistributionConfig: {
DefaultCacheBehavior: {
CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6',
Compress: true,
TargetOriginId: 'StackMyDistOrigin1D6D5E535',
ViewerProtocolPolicy: 'allow-all',
},
Enabled: true,
HttpVersion: 'http2',
IPV6Enabled: true,
Origins: [{
DomainName: 'www.example.com',
Id: 'StackMyDistOrigin1D6D5E535',
CustomOriginConfig: {
OriginProtocolPolicy: 'https-only',
},
}],
},
});
Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::MonitoringSubscription', {
DistributionId: {
Ref: 'MyDistDB88FD9A',
},
MonitoringSubscription: {
RealtimeMetricsSubscriptionConfig: {
RealtimeMetricsSubscriptionStatus: 'Enabled',
},
},
});
});

describe('Distribution metrics tests', () => {
const metrics = [
{ name: 'OriginLatency', method: 'metricOriginLatency', additionalMetricsRequired: true },
{ name: 'CacheHitRate', method: 'metricCacheHitRate', additionalMetricsRequired: true },
...['401', '403', '404', '502', '503', '504'].map(errorCode => ({
name: `${errorCode}ErrorRate`,
method: `metric${errorCode}ErrorRate`,
additionalMetricsRequired: true,
})),
];

test.each(metrics)('get %s metric', (metric) => {
const origin = defaultOrigin();
const dist = new Distribution(stack, 'MyDist', {
defaultBehavior: { origin },
publishAdditionalMetrics: metric.additionalMetricsRequired,
});

const metricObj = dist[metric.method]();

expect(metricObj).toEqual(new cloudwatch.Metric({
namespace: 'AWS/CloudFront',
metricName: metric.name,
dimensions: { DistributionId: dist.distributionId },
statistic: 'Average',
period: Duration.minutes(5),
}));
});

test.each(metrics)('throw error when trying to get %s metric without publishing additional metrics', (metric) => {
const origin = defaultOrigin();
const dist = new Distribution(stack, 'MyDist', {
defaultBehavior: { origin },
publishAdditionalMetrics: false,
});

expect(() => {
dist[metric.method]();
}).toThrow(new RegExp(`${metric.name} metric is only available if 'publishAdditionalMetrics' is set 'true'`));
});
});
Loading