From 8272c348c7e2290046b7ae6812c24b0627d534ff Mon Sep 17 00:00:00 2001 From: Daniel Van Der Ploeg Date: Tue, 9 Jul 2024 11:15:59 +0930 Subject: [PATCH 1/5] feat: add cloudwatch dashboard --- packages/prerender-fargate/lib/monitoring.ts | 515 ++++++++++++++++++ .../lib/prerender-fargate-options.ts | 12 + .../lib/prerender-fargate.ts | 23 +- .../prerender-recache-api-construct.ts | 11 +- 4 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 packages/prerender-fargate/lib/monitoring.ts diff --git a/packages/prerender-fargate/lib/monitoring.ts b/packages/prerender-fargate/lib/monitoring.ts new file mode 100644 index 00000000..b5d02cd2 --- /dev/null +++ b/packages/prerender-fargate/lib/monitoring.ts @@ -0,0 +1,515 @@ +import { Construct } from "constructs"; +import { + Metric, + TextWidget, + SingleValueWidget, + GraphWidget, + LegendPosition, + Dashboard, + Column, + LogQueryWidget, + Alarm, + ComparisonOperator, + LogQueryVisualizationType, + MathExpression, +} from "aws-cdk-lib/aws-cloudwatch"; +import { FargateService } from "aws-cdk-lib/aws-ecs"; +import { + ApplicationLoadBalancer, + HttpCodeElb, + HttpCodeTarget, +} from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import { LogGroup } from "aws-cdk-lib/aws-logs"; +import { Topic } from "aws-cdk-lib/aws-sns"; +import { Bucket } from "aws-cdk-lib/aws-s3"; +import { Queue } from "aws-cdk-lib/aws-sqs"; +import * as lambda from "aws-cdk-lib/aws-lambda"; + +export interface PerformanceMetricsProps { + dashboardName?: string; + service: FargateService; + loadBalancer: ApplicationLoadBalancer; + logGroup: LogGroup; + snsTopic?: Topic; + cacheBucket: Bucket; + recache?: { + queue: Queue; + consumer: lambda.Function; + producer: lambda.Function; + }; + additionalAlarms?: Alarm[]; +} + +export class PerformanceMetrics extends Construct { + constructor(scope: Construct, id: string, props: PerformanceMetricsProps) { + super(scope, id); + + const alarms: Alarm[] = props.additionalAlarms || []; + + // Load balancer metrics and widgets + const requestCountMetrics: Metric[] = [ + props.loadBalancer.metrics.requestCount({ + label: "Total Request Count", + }), + props.loadBalancer.metrics.httpCodeTarget( + HttpCodeTarget.TARGET_2XX_COUNT, + { + label: "HTTP 2xx Count", + color: "#69ae34", + } + ), + props.loadBalancer.metrics.httpCodeElb(HttpCodeElb.ELB_4XX_COUNT, { + label: "ELB 4xx Count", + color: "#f89256", + }), + props.loadBalancer.metrics.httpCodeElb(HttpCodeElb.ELB_5XX_COUNT, { + label: "ELB 5xx Count", + }), + ]; + + const targetResponseTime: Metric = + props.loadBalancer.metrics.targetResponseTime({ + label: "Target Response Time", + }); + + const loadBalancerLabel = new TextWidget({ + markdown: "# Load Balancer", + width: 24, + height: 1, + }); + + const requestCountSingleWidget = new SingleValueWidget({ + title: "Prerender Request Count", + width: 12, + height: 4, + sparkline: true, + metrics: requestCountMetrics, + }); + + const requestCountGraphWidget = new GraphWidget({ + title: "Prerender Request Count", + width: 12, + height: 7, + }); + + const responseTimeGraphWidget = new GraphWidget({ + title: "Target Response Time", + width: 12, + height: 6, + legendPosition: LegendPosition.HIDDEN, + }); + responseTimeGraphWidget.addLeftMetric(targetResponseTime); + + requestCountMetrics.forEach(metric => { + requestCountGraphWidget.addLeftMetric(metric); + }); + + const loadBalancerWidgets = [ + loadBalancerLabel, + requestCountSingleWidget, + requestCountGraphWidget, + responseTimeGraphWidget, + ]; + + // Prerender Performance + const currentActiveTasksMetric: Metric[] = [ + // Current no built in method to get these metrics + // Need to manually construct + new Metric({ + namespace: "ECS/ContainerInsights", + metricName: "DesiredTaskCount", + dimensionsMap: { + ServiceName: props.service.serviceName, + ClusterName: props.service.cluster.clusterName, + }, + statistic: "avg", + }), + new Metric({ + namespace: "ECS/ContainerInsights", + metricName: "RunningTaskCount", + dimensionsMap: { + ServiceName: props.service.serviceName, + ClusterName: props.service.cluster.clusterName, + }, + color: "#69ae34", + statistic: "avg", + }), + new Metric({ + namespace: "ECS/ContainerInsights", + metricName: "PendingTaskCount", + dimensionsMap: { + ServiceName: props.service.serviceName, + ClusterName: props.service.cluster.clusterName, + }, + color: "#f89256", + statistic: "avg", + }), + ]; + + // Alert when 0 tasks are running + alarms.push( + new Alarm(this, "currentActiveTasksAlarm", { + metric: currentActiveTasksMetric[0], + threshold: 0, + comparisonOperator: ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + evaluationPeriods: 1, + datapointsToAlarm: 1, + }) + ); + + const taskCPUMetrics: Metric[] = [ + props.service.metricCpuUtilization({ + statistic: "min", + }), + props.service.metricCpuUtilization({ + statistic: "max", + }), + props.service.metricCpuUtilization({ + statistic: "avg", + }), + ]; + + const taskMemoryMetrics: Metric[] = [ + props.service.metricMemoryUtilization({ + statistic: "min", + }), + props.service.metricMemoryUtilization({ + statistic: "max", + }), + props.service.metricMemoryUtilization({ + statistic: "avg", + }), + ]; + + /** + * PERFORMANCE STATS BLOCK + */ + const prerenderPerformanceLabel = new TextWidget({ + markdown: "# Prerender Performance", + width: 24, + height: 1, + }); + + const currentActiveTasksSingleWidget = new SingleValueWidget({ + title: "Current Active Tasks", + width: 9, + height: 3, + sparkline: false, + metrics: currentActiveTasksMetric, + }); + + const currentActiveTasksGraphWidget = new GraphWidget({ + title: "Current Active Tasks", + width: 9, + height: 5, + }); + currentActiveTasksMetric.slice(1).forEach(metric => { + currentActiveTasksGraphWidget.addLeftMetric(metric); + }); + + const currentTaskWidgets = [ + currentActiveTasksSingleWidget, + currentActiveTasksGraphWidget, + ]; + + const taskCPUGraphWidget = new GraphWidget({ + title: "Prerender CPU", + width: 9, + height: 6, + }); + taskCPUMetrics.forEach(metric => { + taskCPUGraphWidget.addLeftMetric(metric); + }); + + const taskMemoryGraphWidget = new GraphWidget({ + title: "Prerender Memory", + width: 9, + height: 6, + }); + taskMemoryMetrics.forEach(metric => { + taskMemoryGraphWidget.addLeftMetric(metric); + }); + + const prerenderPerformanceMetrics = [ + taskCPUGraphWidget, + taskMemoryGraphWidget, + ]; + + const statusCodes = new LogQueryWidget({ + title: "Response Codes", + width: 6, + height: 6, + queryString: + "fields @timestamp, status | filter ispresent(status) | stats count(status) as `Response Code` by status", + logGroupNames: [props.logGroup.logGroupName], + view: LogQueryVisualizationType.PIE, + }); + + // Top bot refers + const topBotRefers = new LogQueryWidget({ + title: "Top Bot Refers", + width: 9, + height: 9, + queryString: `parse message /User-Agent: "(?.*)"/ | filter ispresent(userAgent) and userAgent not like "ELB-HealthChecker/2.0" | sort @timestamp desc | stats count(userAgent) as countUserAgent by userAgent | sort countUserAgent desc`, + logGroupNames: [props.logGroup.logGroupName], + view: LogQueryVisualizationType.TABLE, + }); + + // Top pages + const topPages = new LogQueryWidget({ + title: "Top Pages", + width: 6, + height: 14, + queryString: + "parse message /(?https:\\/\\/.*)/ | filter message like 'got ' and ispresent(url) | stats count(url) as countUrl by url | sort countUrl desc", + logGroupNames: [props.logGroup.logGroupName], + view: LogQueryVisualizationType.TABLE, + }); + + // Average render time + const avgRenderTimePerHour = new LogQueryWidget({ + title: "Average Render Time (per hour)", + width: 9, + height: 6, + queryString: `parse message /got \\d{3} in (?