Sending Alert Text Messages Based on CloudWatch Log Patterns

Published on December 24, 2021

By Hyuntaek Park

Senior full-stack engineer at Twigfarm

Introduction

Monitoring or getting an alert when unexpected happens is essential to keep our system reliable. System faults should be notified to appropriate person as early as it can be. This can be achieved easily with CloudWatch.

In this post, I create a simple lambda function and a couple of tests with a normal condition and one with an error condition. Once the error condition happens SMS text message is sent to my cell number all the way from North Virginia (us-east-1) to Seoul, Korea (ap-northeast-2).

The idea is simple:

  • Exception happens during lambda execution
  • Logging error in the CloudWatch associated with that lambda
  • CloudWatch invokes another lambda function which sends an SMS text message if a predefined expression is in the log text

Architecture

Creating a simple lambda function

Go to Lambda Functions and click Create function. Then create a simple function with any name you like.

Creating lambda function

The lambda code does simple division. Our goal is to get notified if DivideByZeroException occurs.

// cloudwatch-alert-test lambda code
exports.handler = async (event) => {
  console.log("event: ", JSON.stringify(event));
  const { x, y } = event;

  try {
    const result = divide(x, y);
    return result;
  } catch (e) {
    console.error(e);
    return null;
  }
};

const divide = (dividend, divisor) => {
  if (divisor === 0) {
    throw Error("DivideByZeroException");
  }
  return dividend / divisor;
};

Test input (OK case)

{
  "x": 10,
  "y": 2
}

Logs for OK test case

ok test case

Test input (Error case)

{
  "x": 10,
  "y": 0
}

Logs for error test case

error test case

SMS sender lambda function

Let’s create another lambda function, called sms-sender, which gets triggered if the CloudWatch log contains certain pattern. For now this lambda function does nothing but prints out the input so that we are able to identify what the CloudWatch input format is like.

sms-sender

exports.handler = (event, context) => {
  console.log("event: ", JSON.stringify(event));
};

CloudWatch Subscription Filters

Now CloudWatch Subscription Filter connects following two lambda functions:

  • cloudwatch-alert-test
  • sms-sender

Go to CloudWatch –> Logs –> Log groups –> /aws/lambda/cloudwatch-alert-test CloudWatch Logs Menu

Then click on Subscription filters tab then choose Create Lambda subscription filter CloudWatch Subscription filters

Input as following:

  • Lambda function: sms-sender
  • Log format: Other
  • Subscription filter pattern: DivideByZeroException
  • Subscription filter name: ANY_NAME_YOU_LIKE

Click on Start streaming button to finish CloudWatch subscription filter setup.

This means that sms-sender lambda function gets invoked, if and only if the CloudWatch log contains string DivideByZeroException.

Testings

We created two tests for cloudwatch-alert-test. Run each of them and check the CloudWatch log for sms-sender to see if the function is invoked only when DivideByZeroException is happened.

Input passed from CloudWatch to sms-sender

What do you see in sms-sender CloudWatch log? Was the function triggered only on error test case? Probably yes. Now take a look at the event parameter value in the log. You would see some data value like the following:

CloudWatch logs

What is it? We have to unzip to decode the data.

Decoding input from CloudWatch

Following code takes care of unzipping the input from CloudWatch.

const zlib = require("zlib");

exports.handler = (event, context) => {
  console.log("event: ", JSON.stringify(event));
  const payload = Buffer.from(event.awslogs.data, "base64");

  zlib.gunzip(payload, async (err, result) => {
    if (err) {
      console.error(err);
      context.fail(err);
    } else {
      const stringResult = result.toString("utf-8");
      console.log("stringResult: ", stringResult);
      context.succeed();
    }
  });
};

Variable stringResult shows stringified version of the input, which looks like:

CloudWatch logs

If your alert method requirement is webhook notifications, then you can just add a few lines of code for HTTP POST call in the lambda function. End of story.

Since SMS text message is a means of alert requirement, one more setup is required.

Setting Amazon Simple Notification Service (SNS) for SMS text messaging

As of December 2021, mobile text messaging (SMS) feature is not supported in Seoul region (ap-northeast-2). So I changed my region to complete text messaging setup in SNS.

Supported Regions are listed here: https://docs.aws.amazon.com/sns/latest/dg/sns-supported-regions-countries.html

After changing region to us-east-1, go to Amazon SNS –> Mobile –> Text messaging (SMS). If Text messaging (SMS) is now showing in your region, it is likely that your region is not supporting this feature.

SNS console

Under the Sandbox destination phone numbers, click Add phone number for adding recipients’ phone numbers. By default, you are in a sandbox mode which has a few restrictions such as:

  • Recipients’ phone numbers have to be verified
  • 1 USD is the maximum you can spend. (it suddenly stops sending messages after reaching the limit)

Note: you can always request limit increase here: https://aws.amazon.com/premiumsupport/knowledge-center/sns-sms-spending-limit-increase/

AWS support will get you out of sandbox as well upon the limit request.

After you add a recipient’s phone number for testing, now let’s go back to sms-sender lambda function in order to add some code for text messaging.

Publishing text message code in sms-sender

Following is the complete lambda code for sms-sender, in which message sending feature is added.

const zlib = require("zlib");

const AWS = require("aws-sdk");
AWS.config.update({ region: "us-east-1" });

exports.handler = (event, context) => {
  console.log("event: ", JSON.stringify(event));
  const payload = Buffer.from(event.awslogs.data, "base64");

  zlib.gunzip(payload, async (err, result) => {
    if (err) {
      console.error(err);
      context.fail(err);
    } else {
      const stringResult = result.toString("utf-8");
      console.log("stringResult: ", stringResult);
      await sendMessages(JSON.parse(stringResult));
      context.succeed();
    }
  });
};

const sendMessages = async (data) => {
  const sns = new AWS.SNS();
  const recipientNumbers = [YOUR_NUMBERS];

  try {
    for (const PhoneNumber of recipientNumbers) {
      console.log("sending to: " + PhoneNumber);
      await sns
        .publish({
          Message: `Please check ${data.logGroup}\n${data.logStream}`,
          PhoneNumber,
        })
        .promise();
    }
  } catch (err) {
    console.error("ERROR: ", err);
  }
};

Almost done. One last adjustment is left. Our lambda role needs a permission to send text messages.

IAM role for text messaging

Let’s move to IAM role for sms-sender lambda function.

IAM role for sms-sender

In the lambda role, click Attach policies –> check AmazonSNSFullAccess –> click Attach policy button.

Final testing

Now all of our components are set up and ready. Go back to cloudwatch-alert-test lambda function and run error test case. Wait a few seconds, you should receive a text message if you set up everything correctly. You can modify the message to any way that you like to. Note that if your message is longer than 160 characters, they are split into separate messages.

Conclusion

You can design your alert system in various ways by applying different log patterns without much hassle utilizing CloudWatch subscription filters.

Getting alerts if something goes wrong is very useful. But it might be annoying if our phones vibrate too frequently than it should be. Yes, it is very tricky to get the balance right. I know DivideByZeroException is not an appropriate case to get SMS text messaging alert :)

Each system has different reliability requirements. When to fire alarm should be designed carefully according to the requirements. Avoiding false alarms is crucial point to consider as well.

Happy Christmas and to our reliable systems!