Photo by path digital on Unsplash

Auto-Generated AWS CloudWatch Dashboards using the AWS CDK

A hands-on example of autogenerating CloudWatch dashboards and widgets via L3 CDK constructs, with example repo written in TypeScript and the AWS CDK.

Serverless Advocate
16 min readDec 20, 2023

--

Preface

  • We talk through the benefits of CloudWatch Dashboards. 📊
  • We discuss the code for the article which is written in TypeScript. ⌨️

Introduction 👋🏽

In a previous article, we looked at monitoring our Serverless solutions using Amazon CloudWatch Alarms and Custom Metrics; enabling us to trigger alarms when specific custom criteria were met.

In this article, we delve deeper into the topic by demonstrating the creation of custom AWS CDK L3 Constructs with integrated CloudWatch Dashboards that are created automatically. This example illustrates how embedding these dashboards and widgets as a standard feature within your custom constructs can enhance the developer experience and streamline the workflow for all consumers, alleviating the cognitive burden on engineering teams.

We will talk through a fictitious company called ‘L.J Travel’ which allows customers to book holidays online.

Our fictitious company, L.J Travel

The code repository for this article can be found here:

To talk through the code we are going to create this very simple serverless architecture:

We are building a small app to showcase custom constructs in the AWS CDK

We can see that:

  • Customers interact with our Amazon API Gateway to book holidays.
  • There is a Lambda function integration which for this demo can randomly throw errors.
  • Successful bookings are persisted to Amazon DynamoDB.
  • The engineering team have an autogenerated Amazon CloudWatch dashboard showing alarms and information for all three AWS services mentioned above.

As we can see from the image below, customers typically use the L.J Travel website without any issues; however, on some occasions when there are issues the engineering team get email alerts. The team can also monitor the service using the CloudWatch dashboard back at the office on a large screen.

So, we have talked a lot about CloudWatch Dashboards, but let’s dive into what they are, and why we want to auto-generate them!

What is a CloudWatch Dashboard? 📊

Let’s first cover what Amazon CloudWatch Dashboards are, and how they can support us in monitoring our serverless solutions health.

Example screenshot of our auto-generated CloudWatch dashboard with widgets for many services

Amazon CloudWatch dashboards are customisable home pages in the CloudWatch console that you can use to monitor your resources in a single view, even those resources that are spread across different Regions. You can use CloudWatch dashboards to create customised views of the metrics and alarms for your AWS resources (in our example this is all of the services that make up the L.J Travel service).

With dashboards, you can create the following as examples:

  • A single view for selected metrics and alarms to help you assess the health of your resources and applications across one or more Regions. You can select the colour used for each metric on each graph so that you can easily track the same metric across multiple graphs.
  • An operational playbook that guides team members during operational events about how to respond to specific incidents.
  • A common view of critical resource and application measurements that can be shared by team members for faster communication flow during operational events.

Note: It is common to have a dashboard on a large screen for office based staff showing the status of critical workloads.

If you have multiple AWS accounts, you can also set up CloudWatch cross-account observability and then create rich cross-account dashboards in your monitoring accounts.

Now that we understand what CloudWatch dashboards are, let’s talk through the key code for this article.

👇 Before we go any further — please connect with me on LinkedIn for future blog posts and Serverless news https://www.linkedin.com/in/lee-james-gilmore/

Talking through key code 👨‍💻

Let’s start with one of the key pieces of the puzzle, which is our configuration to be used across the solution.

Configuration

For this, I almost always reach out for convict, which allows us to store configuration in one place; which also allows us to pull in environment variables in a concise way:

const convict = require('convict');

export const config = convict({
tableName: {
doc: 'The database table where we store holidays',
format: String,
default: 'tableName',
env: 'TABLE_NAME',
},
randomErrorBool: {
doc: 'Whether or not we throw random errors',
format: String,
default: 'false',
env: 'RANDOM_ERROR',
},
email: {
doc: 'The email for the sns topic',
format: String,
default: 'your.email@gmail.com',
},
namespace: {
doc: 'The namespace for the metrics',
format: String,
default: 'HolidaysNamespace',
},
service: {
doc: 'The service for the metrics',
format: String,
default: 'TravelService',
},
region: {
doc: 'The region for the solution',
format: String,
default: 'eu-west-1',
},
}).validate({ allowed: 'strict' });

We can see in the config above that we specifically have values for:

  • Metric Service — A service in CloudWatch refers to a specific AWS service or resource that generates metrics. Each AWS service that integrates with CloudWatch produces its own set of metrics, which can be monitored and analysed. In our example, these are our custom metrics (as discussed in the previous article). We have this set to TravelService in the config.
  • Metric Namespace — A namespace is a container for CloudWatch metrics. It acts as a grouping mechanism for metrics related to a specific service or application. In our example, this is set to HolidaysNamespace.
  • Region — AWS resources, including CloudWatch metrics, are associated with a specific AWS region. In our example we are using eu-west-1.

Finally a quick mention of the randomError config value. This value allows us to turn off or on the config for randomly generating errors in our Lambda function by changing the environment variable in the Lambda. This allows us to see the effects on the dashboard easily.

Custom Stack

Now let’s have a look at our CustomStack code as shown below.

import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

import { Construct } from 'constructs';
import { CloudwatchDashboard } from './cloudwatch-dashboard';

export interface CustomStackProps extends cdk.StackProps {
dashboardName?: string;
dashboardDescription?: string;
}

export class CustomStack extends cdk.Stack {
private readonly dashboard: cloudwatch.Dashboard;
public readonly widgets: cloudwatch.ConcreteWidget[] = [];
private readonly createDashboard: boolean;

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

this.createDashboard =
props?.dashboardName && props?.dashboardDescription ? true : false;

// if dashboard properties have been added then create a dashboard
if (this.createDashboard) {
this.dashboard = new CloudwatchDashboard(this, id + 'Dashboard', {
dashboardName: props?.dashboardName as string,
dashboardDescription: props?.dashboardDescription as string,
}).dashboard;
}
}

public addWidget(widget: cloudwatch.ConcreteWidget): void {
this.widgets.push(widget);
if (this.createDashboard) {
this.dashboard.addWidgets(widget);
}
}

public addWidgets(widgets: cloudwatch.ConcreteWidget[]): void {
this.widgets.push(...widgets);

if (this.createDashboard) {
this.dashboard.addWidgets(...widgets);
}
}

public getWidgets(): cloudwatch.ConcreteWidget[] {
return this.widgets;
}

public getDashboard(): cloudwatch.Dashboard {
return this.dashboard;
}
}

We can see from looking at the code above that we have extended the base cdk.Stack to allow us to add some additional properties and methods. We have a private property called widgets which allows us through the public methods to add widgets (or a single widget) to this stack. The beauty of this is that anytime we use our custom stack we don’t need to think about how we deal with CloudWatch widgets, we simply use the addWidgets, addWidget or getWidgets methods.

It is also key to note that if the consumer passes in props for dashboardName and dashboardDescription we will also automatically create a dashboard which the widgets will be added to when created.

Once again, this means that we have taken the cognitive load off the consumers of the CustomStack, and they can simply use the methods provided on both our stateless and stateful stacks.

CloudWatch Dashboard Custom Construct

Next we have created a custom L3 construct called CloudwatchDashboard for creating a CloudWatch dashboard which always has a title and text description at the top. These were passed through into our CustomStack earlier.

import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

import { Construct } from 'constructs';

interface DashboardProps extends cloudwatch.DashboardProps {
/**
* The dashboard description in markdown
*/
dashboardDescription: string;
}

export class CloudwatchDashboard extends Construct {
public readonly dashboard: cloudwatch.Dashboard;

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

// add the dashboard
this.dashboard = new cloudwatch.Dashboard(this, id + 'Dashboard', {
...props,
});
this.dashboard.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

// add the title text widget as standard
this.dashboard.addWidgets(
new cloudwatch.TextWidget({
markdown: props.dashboardDescription,
width: 24,
height: 2,
})
);
}
}

DynamoDB L3 Construct

Now for the fun part! We create our first custom L3 construct for a DynamoDB table which we have called DynamoDbTable, which also automatically creates our widgets which we can add to the dashboard (in this example to monitor the read and write capacity units):

import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

import { Construct } from 'constructs';

interface DynamoDbTableProps
extends Pick<dynamodb.TableProps, 'removalPolicy' | 'partitionKey'> {
/**
* whether to create the widget or not
*/
createWidget?: boolean;
/**
* The partition key attribute for the table
*/
partitionKey: dynamodb.Attribute;
/**
* The removal policy for the table
*/
removalPolicy: cdk.RemovalPolicy;
/**
* The widget title
*/
widgetTitle: string;
}

type FixedDynamoDbTableProps = Omit<
dynamodb.TableProps,
'removalPolicy' | 'partitionKey'
>;

export class DynamoDbTable extends Construct {
public readonly table: dynamodb.Table;
public readonly widget: cloudwatch.GraphWidget;

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

const createWidget = props?.createWidget === true ? true : false;

const fixedProps: FixedDynamoDbTableProps = {
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
pointInTimeRecovery: true,
contributorInsightsEnabled: true,
};

this.table = new dynamodb.Table(this, id + 'Table', {
// fixed props
...fixedProps,
// custom props
...props,
});

if (createWidget) {
// add the widget automatically
this.widget = new cloudwatch.GraphWidget({
title: props.widgetTitle,
left: [this.table.metricConsumedWriteCapacityUnits()],
right: [this.table.metricConsumedReadCapacityUnits()],
statistic: cloudwatch.Stats.SUM,
period: cdk.Duration.minutes(1),
});
}
}
}

Note: We have a property called createWidget which allows the consumer of the construct to choose whether to create them automatically or not when using the custom L3 constructs.

Stateful Stack

In our TravelServiceStatefulStack which extends the CustomStack we created earlier, we can then utilise this construct and add our autogenerated dashboard widgets to the stack through the addWidget method.

import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

import { CustomStack, DynamoDbTable } from '../custom-constructs';

import { Construct } from 'constructs';

export class TravelServiceStatefulStack extends CustomStack {
public readonly table: dynamodb.Table;

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

// use our custom L3 construct which creates the table and the dashboard widget
const { table, widget } = new DynamoDbTable(this, 'HolidaysTable', {
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
widgetTitle: 'DynamoDB Table Metrics',
createWidget: true,
});

this.table = table;

// add our widget to the stack
this.addWidget(widget);
}
}

Notice that in the previous code snippet, we added the widgets for the StatefulStack, but we don’t want a separate dashboard just for DynamoDB. This is where we configured this properly through the main CDK App as shown below:

#!/usr/bin/env node

import 'source-map-support/register';

import * as cdk from 'aws-cdk-lib';

import { TravelServiceStatefulStack } from '../stateful/stateful';
import { TravelServiceStatelessStack } from '../stateless/stateless';

const app = new cdk.App();
const travelServiceStatefulStack = new TravelServiceStatefulStack(
app,
'TravelServiceStatefulStack',
{}
);
const travelServiceStatelessStack = new TravelServiceStatelessStack(
app,
'TravelServiceStatelessStack',
{
table: travelServiceStatefulStack.table,
dashboardName: 'TravelServiceDashboard', // create a dashboard automatically
dashboardDescription: '# LJ Travel\nMetrics Dashboard',
}
);

// add any sibling widgets
travelServiceStatelessStack.addWidgets(travelServiceStatefulStack.getWidgets());

We can see from the code above that we simply pass any widgets created on the Stateful Stack through to the Stateless Stack; and as we only added dashboard properties for the Stateless Stack, this is when the dashboard is created. We then add these Stateful Stack sibling widgets onto the Stateless Stack using the addWidgets method, therefore having all autogenerated widgets on one dashboard.

Stateless Stack

If we now look at the Stateless Stack which extends our CustomStack that we created earlier, we can see that there is no mention of CloudWatch dashboards or widgets at all, and these are automatically created in the background using a combination of our custom L3 constructs, and our public methods we created earlier!

Note: In the code repo we also have custom L3 constructs for API Gateway and Lambda functions in a similar way to the DynamoDB table example above; but with their own widget properties.

import * as cdk from 'aws-cdk-lib';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as snsSubs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as path from 'path';

import { Api, CustomStack, LambdaFunction } from '../custom-constructs';

import { Construct } from 'constructs';
import { CustomStackProps } from 'custom-constructs/custom-stack';
import { config } from '../stateless/src/config';

export interface StatelessStackProps extends CustomStackProps {
table: dynamodb.Table;
}

export class TravelServiceStatelessStack extends CustomStack {
public readonly table: dynamodb.Table;

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

this.table = props.table;

// get all of the configuration values
const { email, namespace, service, region, randomErrorBool } =
config.getProperties();

const lambdaPowerToolsConfig = {
LOG_LEVEL: 'DEBUG',
POWERTOOLS_LOGGER_LOG_EVENT: 'true',
POWERTOOLS_LOGGER_SAMPLE_RATE: '1',
POWERTOOLS_TRACE_ENABLED: 'enabled',
POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS: 'captureHTTPsRequests',
POWERTOOLS_SERVICE_NAME: service,
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'captureResult',
POWERTOOLS_METRICS_NAMESPACE: namespace,
};

// create our sns topic for our alarm
const topic = new sns.Topic(this, 'AlarmTopic', {
displayName: 'ErrorAlarmTopic',
topicName: 'ErrorAlarmTopic',
});

// create a lambda function with our custom construct which
// also creates the dashboard widgets for us automatically
const {
function: bookHolidayLambda,
widgets: bookHolidayLambdaAlarmWidgets,
} = new LambdaFunction(this, 'BookHolidayLambda', {
entry: path.join(
__dirname,
'src/adapters/primary/book-holiday/book-holiday.adapter.ts'
),
environment: {
RANDOM_ERROR: randomErrorBool,
TABLE_NAME: this.table.tableName,
...lambdaPowerToolsConfig,
},
topic,
region,
createWidget: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
alarmName: 'BookHolidayErrorAlarm',
alarmDescription: 'Book Holiday Error Alarm',
metricName: 'BookHolidayError',
metricNamespace: namespace,
metricsService: service,
metricErrorName: 'BookHolidayError',
metricErrorNameTitle: 'Book Holiday Errors',
metricSuccessName: 'SuccessfulHolidayBooking',
metricSuccessNameTitle: 'Successful Holiday Bookings',
metricFilterPattern:
'{ $.statusCode = 400 && $.errorName = "BookHolidayError" }',
filterName: 'BookHolidayErrorMetricFilter',
});
this.addWidgets(bookHolidayLambdaAlarmWidgets);

// grant the lambda write access to the table
this.table.grantWriteData(bookHolidayLambda);

// add the email subscription for our alarms so we are alerted
topic.addSubscription(new snsSubs.EmailSubscription(email));

// create our rest api using our custom construct which also creates are widgets
const { api, widgets: ApiWidgets } = new Api(this, 'HolidaysApi', {
description: 'Holidays API',
widgetTitle: 'Travel API Metrics',
deploy: true,
createWidget: true,
});
this.addWidgets(ApiWidgets);

// add our routes and integrations to the api
const holidays: apigw.Resource = api.root.addResource('holidays');
holidays.addMethod(
'POST',
new apigw.LambdaIntegration(bookHolidayLambda, {
proxy: true,
})
);
}
}

The only methods that we actually call are this.addWidgets(widgets) when our custom L3 constructs automatically create them for us.

Book Holiday Lambda Function

In our primary adapter for our Lambda function, we can see that we utilise the aws-lambda-powertools npm package to support us in using logging, tracing and custom metrics.

import {
MetricUnits,
Metrics,
logMetrics,
} from '@aws-lambda-powertools/metrics';
import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
import { errorHandler, logger, randomError, schemaValidator } from '@shared';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import { config } from '@config';
import { BookHolidayDto } from '@dto/book-holiday';
import { ValidationError } from '@errors';
import middy from '@middy/core';
import { bookHolidayUseCase } from '@use-cases/book-holiday';
import { schema } from './book-holiday.schema';

const tracer = new Tracer({
serviceName: config.get('service'),
});
const metrics = new Metrics({
namespace: config.get('namespace'),
serviceName: config.get('service'),
});

export const bookHolidayAdapter = async ({
body,
}: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
if (!body) throw new ValidationError('no payload body');

const holiday = JSON.parse(body) as BookHolidayDto;

// randomly throw a 'BookHolidayError' error for the purpose of the demo
randomError();

schemaValidator(schema, holiday);

const created: BookHolidayDto = await bookHolidayUseCase(holiday);

metrics.addMetric('SuccessfulHolidayBooking', MetricUnits.Count, 1);

return {
statusCode: 201,
body: JSON.stringify(created),
};
} catch (error) {
let errorMessage = 'Unknown error';
if (error instanceof Error) errorMessage = error.message;
logger.error(errorMessage);

metrics.addMetric('BookHolidayError', MetricUnits.Count, 1);

return errorHandler(error);
}
};

export const handler = middy(bookHolidayAdapter)
.use(injectLambdaContext(logger))
.use(captureLambdaHandler(tracer))
.use(logMetrics(metrics));

We simply pass through the configuration for using Powertools using the config (backed by convict) that we created earlier; which also allows us to toggle on and off the throwing of random errors as shown in the code above. On a successful run of the function we will call:

metrics.addMetric('SuccessfulHolidayBooking', MetricUnits.Count, 1);

and on error, we will call:

metrics.addMetric('BookHolidayError', MetricUnits.Count, 1);

These custom metrics drive our Amazon CloudWatch alarm which is shown on the dashboard, as well as the display of how many errors and successful runs we have in realtime.

Error Handler

We also have a custom error handler which we use in conjunction with Middy, which allows us to log the specific error and status code for us to produce metric filters on.

import { logger } from '@shared';
import { APIGatewayProxyResult } from 'aws-lambda';

// we would typically use middy - but to keep this simple to read
// without mutliple additional packages lets build outselves
export function errorHandler(error: Error | unknown): APIGatewayProxyResult {
console.error(error);

let errorMessage: string;
let statusCode: number;

if (error instanceof Error) {
switch (error.name) {
case 'BookHolidayError': // note: this is our error type we want to alert on
case 'ValidationError':
errorMessage = error.message;
statusCode = 400;
break;
case 'ResourceNotFound':
errorMessage = error.message;
statusCode = 404;
break;
default:
errorMessage = 'An error has occurred';
statusCode = 500;
break;
}
logger.error(errorMessage, {
errorName: error.name, // these additional props in the logs allow us to filter them
statusCode,
});
} else {
errorMessage = 'An error has occurred';
statusCode = 500;

logger.error(errorMessage, {
errorName: 'UnknownError',
statusCode,
});
}

return {
statusCode: statusCode,
body: JSON.stringify({
message: errorMessage,
}),
};
}

Lambda Function L3 Construct

One last piece of the puzzle for automatically creating CloudWatch widgets and alarms for our Lambda functions is our custom L3 construct:

import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as sns from 'aws-cdk-lib/aws-sns';

import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { Construct } from 'constructs';

interface LambdaFunctionProps
extends Omit<
nodeLambda.NodejsFunctionProps,
| 'removalPolicy'
| 'runtime'
| 'memorySize'
| 'timeout'
| 'tracing'
| 'handler'
| 'bundling'
> {
/**
* whether to create the widget or not
*/
createWidget?: boolean;
/**
* The removal policy
*/
removalPolicy: cdk.RemovalPolicy;
/**
* The The cloudwatch metric namespace
*/
metricNamespace: string;
/**
* The cloudwatch metric name
*/
metricName: string;
/**
* The cloudwatch filter name
*/
filterName: string;
/**
* The cloudwatch filter pattern
*/
metricFilterPattern: string;
/**
* The cloudwatch alarm name
*/
alarmName: string;
/**
* The cloudwatch alarm description
*/
alarmDescription: string;
/**
* The cloudwatch alarm description
*/
topic: sns.Topic;
/**
* The cloudwatch metrics service name
*/
metricsService: string;
/**
* The region
*/
region: string;
/**
* The metric success name
*/
metricSuccessName: string;
/**
* The metric success name title
*/
metricSuccessNameTitle: string;
/**
* The metric error name
*/
metricErrorName: string;
/**
* The metric error name title
*/
metricErrorNameTitle: string;
}

export class LambdaFunction extends Construct {
public readonly function: nodeLambda.NodejsFunction;
public readonly widgets: cloudwatch.ConcreteWidget[] = [];
public readonly metricFilter: logs.MetricFilter;
public readonly alarm: cloudwatch.Alarm;

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

const createWidget = props?.createWidget === true ? true : false;

// these are our fixed props as an example only
const fixedProps = {
runtime: lambda.Runtime.NODEJS_20_X,
memorySize: 1024,
timeout: cdk.Duration.seconds(5),
tracing: lambda.Tracing.ACTIVE,
handler: 'handler',
bundling: {
minify: true,
externalModules: [],
},
};

this.function = new nodeLambda.NodejsFunction(this, id + 'Lambda', {
// fixed props
...fixedProps,
// custom props
...props,
});

// create the cloudwatch custom metric filter
this.metricFilter = this.function.logGroup.addMetricFilter(id + 'Filter', {
filterPattern: logs.FilterPattern.literal(props.metricFilterPattern),
metricName: props.metricName,
metricNamespace: props.metricNamespace,
filterName: props.filterName,
});

// create the cloudwatch alarm
this.alarm = new cloudwatch.Alarm(this, id + 'Alarm', {
alarmName: props.alarmName,
alarmDescription: props.alarmDescription,
metric: this.metricFilter.metric(),
threshold: 1,
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
evaluationPeriods: 1,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
actionsEnabled: true,
});
this.alarm.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
this.alarm.addAlarmAction(new SnsAction(props.topic));

if (createWidget) {
// create the widgets automatically that can be pushed to a dashboard
this.widgets.push(
...[
new cloudwatch.SingleValueWidget({
title: props.metricSuccessNameTitle,
metrics: [
new cloudwatch.Metric({
namespace: props.metricNamespace,
metricName: props.metricSuccessName,
label: props.metricSuccessName,
region: props.region,
dimensionsMap: {
service: props.metricsService,
},
statistic: cloudwatch.Stats.SUM,
period: cdk.Duration.minutes(1),
}),
],
}),
new cloudwatch.SingleValueWidget({
title: props.metricErrorNameTitle,
metrics: [
new cloudwatch.Metric({
namespace: props.metricNamespace,
metricName: props.metricErrorName,
label: props.metricErrorName,
region: props.region,
dimensionsMap: {
service: props.metricsService,
},
statistic: cloudwatch.Stats.SUM,
period: cdk.Duration.minutes(1),
}),
],
}),
new cloudwatch.AlarmStatusWidget({
title: 'Alarms',
alarms: [this.alarm],
}),
]
);
}
}
}

We can see from the code above that we automatically create a CloudWatch alarm for this Lambda function, and the custom metric filter based on the logs from our previous code for it to alert on.

We also create some custom widgets based on the same success and error metrics that we created in our Lambda function code too.

Using our CustomStack and custom L3 constructs for API Gateway, Lambda and DynamoDB produces the following basic CloudWatch dashboard automatically in the background as shown below!

Example screenshot of our auto-generated CloudWatch dashboard with widgets for many services

Wrapping up 👋🏽

I hope you enjoyed this article, and if you did then please feel free to share and feedback!

Please go and subscribe on my YouTube channel for similar content!

I would love to connect with you also on any of the following:

https://www.linkedin.com/in/lee-james-gilmore/
https://twitter.com/LeeJamesGilmore

If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋

Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)

About me

Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect, and Global Head of Technology & Architecture based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.

I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture, and technology.

*** The information provided are my own personal views and I accept no responsibility for the use of the information. ***

You may also be interested in the following:

--

--

Global Head of Technology & Architecture | Serverless Advocate | Mentor | Blogger | AWS x 7 Certified 🚀