in cdk/lib/commercial-canaries.ts [28:153]
constructor(scope: App, id: string, props: Props) {
super(scope, id, props);
const { env, stage, articleUrl, frontUrl } = props;
if (!env?.region) {
throw new Error('env.region is required');
}
const accountId = this.account;
const s3BucketNameCanary = `cw-syn-canary-${accountId}-${env.region}`;
const s3BucketNameResults = `cw-syn-results-${accountId}-${env.region}`;
const isTcf = env.region === 'eu-west-1' || env.region === 'ca-central-1';
const commonCanaryProps: synthetics.CanaryProps = {
test: synthetics.Test.custom({
code: synthetics.Code.fromBucket(
Bucket.fromBucketName(this, 'CanaryS3Bucket', s3BucketNameCanary),
`${stage.toUpperCase()}/nodejs.zip`,
),
handler: 'testPage.handler',
}),
runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_9_1,
schedule: synthetics.Schedule.rate(Duration.minutes(1)),
// Don't run non-prod canaries indefinitely
timeToLive: stage === 'PROD' ? undefined : Duration.minutes(30),
timeout: Duration.seconds(60),
memory: Size.mebibytes(isTcf ? 2048 : 3008),
provisionedResourceCleanup: true,
successRetentionPeriod: Duration.days(7),
failureRetentionPeriod: Duration.days(31),
startAfterCreation: true,
};
const resultsBucket = Bucket.fromBucketName(
this,
'CanaryArtifactsS3Bucket',
`${s3BucketNameResults}`,
);
const canaryFront = new synthetics.Canary(this, 'CanaryFront', {
...commonCanaryProps,
// Limitation of max 21 characaters and lower case. Pattern: ^[0-9a-z_\-]+$
canaryName: `commercial_canary_${stage.toLocaleLowerCase()}_front`,
artifactsBucketLocation: {
bucket: resultsBucket,
prefix: `${stage.toUpperCase()}/front`,
},
environmentVariables: {
logAllRequests: 'false',
logAllResponses: 'false',
pageType: 'front',
url: frontUrl,
},
});
const canaryArticle = new synthetics.Canary(this, 'CanaryArticle', {
...commonCanaryProps,
// Limitation of max 21 characaters and lower case. Pattern: ^[0-9a-z_\-]+$
canaryName: `commercial_canary_${stage.toLocaleLowerCase()}_article`,
artifactsBucketLocation: {
bucket: resultsBucket,
prefix: `${stage.toUpperCase()}/article`,
},
environmentVariables: {
logAllRequests: 'false',
logAllResponses: 'false',
pageType: 'article',
url: articleUrl,
},
});
/** Ensures the canary is redeployed with every code change, since the code lives in S3 separate to the canary itself */
const buildId = new CfnParameter(this, 'BuildId', {
type: 'String',
description:
'The riff-raff build id, automatically generated and provided by riff-raff',
});
Tags.of(canaryFront).add('buildId', buildId.valueAsString);
Tags.of(canaryArticle).add('buildId', buildId.valueAsString);
const topic = new Topic(this, 'Topic');
new Subscription(this, 'Subscription', {
topic,
endpoint: `commercial.canaries+${stage}-${env.region}@guardian.co.uk`,
protocol: SubscriptionProtocol.EMAIL,
region: env.region,
});
/**
* Metric representing the canary success rate per minute, where missing data is filled in with zeros
* @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_synthetics-readme.html#alarms
*/
const alarmMetric = new MathExpression({
label: 'successRate',
expression: 'AVG([FILL(frontSuccess,0), FILL(articleSuccess,0)])',
period: Duration.minutes(1),
usingMetrics: {
frontSuccess: canaryFront.metricSuccessPercent({
statistic: Stats.AVERAGE,
period: Duration.minutes(1),
}),
articleSuccess: canaryArticle.metricSuccessPercent({
statistic: Stats.AVERAGE,
period: Duration.minutes(1),
}),
},
});
const alarm = new Alarm(this, 'Alarm', {
// Only allow alarm actions in PROD
actionsEnabled: stage === 'PROD',
alarmDescription: `Commercial canary is failing in ${env.region}.\nSee https://metrics.gutools.co.uk/d/aej6crz3hvz0gc/canary-status for details`,
alarmName: `commercial-canary-${stage}`,
metric: alarmMetric,
/** Alarm is triggered if canary fails (or fails to run) 5 times in a row */
datapointsToAlarm: 5,
evaluationPeriods: 5,
threshold: 100, // the metric is either 100% or 0% when evaluating minute-by-minute
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
treatMissingData: TreatMissingData.BREACHING,
});
alarm.addAlarmAction(new SnsAction(topic));
alarm.addOkAction(new SnsAction(topic));
}