Skip to content

Commit

Permalink
Add test cases, improved error handling & logging of events
Browse files Browse the repository at this point in the history
The lambda gets local tests we can use to confirm we're handling events
correctly.  In particular, events generated by Log Metrics appear to
have a slightly different format.

Response returned by the script now provides insight into the webhook
response from Slack.

We also have a new env var supported that allows us to enable incoming
event logging for all events.  Even if this is off, failure to handle an
event should still result in the event message being logged.
  • Loading branch information
jinnko committed Feb 13, 2019
1 parent b926f04 commit d935666
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ terraform.tfstate
*.tfstate*
terraform.tfvars
functions/*.zip
functions/pytest.ini
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AWS Notify Slack Terraform module

This module creates SNS topic (or use existing one) and a AWS Lambda function which sends notifications to Slack using [incoming webhooks API](https://api.slack.com/incoming-webhooks).
This module creates an SNS topic (or uses an existing one) and an AWS Lambda function that sends notifications to Slack using the [incoming webhooks API](https://api.slack.com/incoming-webhooks).

Start by setting up an [incoming webhook integration](https://my.slack.com/services/new/incoming-webhook/) in your Slack workspace.

Expand All @@ -11,8 +11,10 @@ Start by setting up an [incoming webhook integration](https://my.slack.com/servi
- [x] Support plaintext and encrypted version of Slack webhook URL
- [x] Most of Slack message options are customizable
- [x] Support different types of SNS messages:
- [x] AWS Cloudwatch
- [x] AWS CloudWatch Alarms
- [x] AWS CloudWatch LogMetrics Alarms
- [ ] [Send pull-request to add support of other message types](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/pulls)
- [x] Local pytest driven testing of the lambda to a Slack sandbox channel

## Usage

Expand All @@ -30,14 +32,27 @@ module "notify_slack" {

## Use existing SNS topic or create new

If you want to subscribe AWS Lambda Function created by this module to an existing SNS topic you should specify `create_sns_topic = false` as argument and specify name of existing SNS topic name in `sns_topic_name`.
If you want to subscribe the AWS Lambda Function created by this module to an existing SNS topic you should specify `create_sns_topic = false` as an argument and specify the name of existing SNS topic name in `sns_topic_name`.

## Examples

* [notify-slack-simple](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/examples/notify-slack-simple) - Creates SNS topic which sends messages to Slack channel.
* [notify-slack-kms](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/examples/notify-slack-simple) - Creates SNS topic which sends messages to Slack channel (using KMS to encrypt Slack webhook URL).
* [cloudwatch-alerts-to-slack](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/examples/cloudwatch-alerts-to-slack) - End to end example which shows how to send AWS Cloudwatch alerts to Slack channel and use KMS to encrypt webhook URL.

## Testing with pytest

To run the tests:

1. Set up a dedicated slack channel as a test sandbox with it's own webhook. See [Slack Incoming Webhooks docs](https://api.slack.com/incoming-webhooks) for details.
2. Make a copy of the sample pytest configuration and edit as needed.

cp functions/pytest.ini.sample functions/pytest.ini

3. Run the tests:

pytest functions/notify_slack_test.py

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->

## Inputs
Expand All @@ -47,13 +62,14 @@ If you want to subscribe AWS Lambda Function created by this module to an existi
| create | Whether to create all resources | string | `true` | no |
| create_sns_topic | Whether to create new SNS topic | string | `true` | no |
| create_with_kms_key | Whether to create resources with KMS encryption | string | `false` | no |
| kms_key_arn | ARN of the KMS key used for decrypting slack webhook url | string | `` | no |
| kms_key_arn | ARN of the KMS key used for decrypting slack webhook url | string | -| no |
| lambda_function_name | The name of the Lambda function to create | string | `notify_slack` | no |
| slack_channel | The name of the channel in Slack for notifications | string | - | yes |
| slack_emoji | A custom emoji that will appear on Slack messages | string | `:aws:` | no |
| slack_username | The username that will appear on Slack messages | string | - | yes |
| slack_webhook_url | The URL of Slack webhook | string | - | yes |
| sns_topic_name | The name of the SNS topic to create | string | - | yes |
| log_events | Boolean flag to enabled/disable logging of incoming events | bool | `false` | no |

## Outputs

Expand Down
25 changes: 21 additions & 4 deletions functions/notify_slack.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import print_function
from urllib.error import HTTPError
import os, boto3, json, base64
import urllib.request, urllib.parse
import logging
Expand Down Expand Up @@ -59,6 +60,11 @@ def notify_slack(message, region):
"icon_emoji": slack_emoji,
"attachments": []
}

if isinstance(message, str):
logging.info("message is a string, assuming it's JSON and decoding")
message = json.loads(message)

if "AlarmName" in message:
notification = cloudwatch_notification(message, region)
payload['text'] = "AWS CloudWatch notification - " + message["AlarmName"]
Expand All @@ -69,14 +75,25 @@ def notify_slack(message, region):

data = urllib.parse.urlencode({"payload": json.dumps(payload)}).encode("utf-8")
req = urllib.request.Request(slack_url)
urllib.request.urlopen(req, data)

try:
result = urllib.request.urlopen(req, data)
return json.dumps({"code": result.getcode(), "info": result.info().as_string()})

except HTTPError as e:
logging.error("{}: result".format(e))
return json.dumps({"code": e.getcode(), "info": e.info().as_string()})


def lambda_handler(event, context):
if 'LOG_EVENTS' in os.environ and os.environ['LOG_EVENTS'] == 'True':
logging.warning('Event logging enabled: `{}`'.format(json.dumps(event)))

message = event['Records'][0]['Sns']['Message']
region = event['Records'][0]['Sns']['TopicArn'].split(":")[3]
notify_slack(message, region)
response = notify_slack(message, region)

return message
if json.loads(response)["code"] != 200:
logging.error("Error: received status `{}` using event `{}` and context `{}`".format(info, event, context))

#notify_slack({"AlarmName":"Example","AlarmDescription":"Example alarm description.","AWSAccountId":"000000000000","NewStateValue":"ALARM","NewStateReason":"Threshold Crossed","StateChangeTime":"2017-01-12T16:30:42.236+0000","Region":"EU - Ireland","OldStateValue":"OK"}, "eu-west-1")
return response
69 changes: 69 additions & 0 deletions functions/notify_slack_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env pytest

import notify_slack
import pytest
from json import loads
from os import environ

events = (
(
{
"Records": [
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:eu-west-2:735598076380:service-updates:d29b4e2c-6840-9c4e-ceac-17128efcc337",
"Sns": {
"Type": "Notification",
"MessageId": "f86e3c5b-cd17-1ab8-80e9-c0776d4f1e7a",
"TopicArn": "arn:aws:sns:eu-west-2:735598076380:service-updates",
"Subject": "OK: \"DBMigrationRequired\" in EU (London)",
"Message": "{\"AlarmName\":\"DBMigrationRequired\",\"AlarmDescription\":\"App is reporting \\\"A JPA error occurred (Unable to build EntityManagerFactory)\\\"\",\"AWSAccountId\":\"735598076380\",\"NewStateValue\":\"OK\",\"NewStateReason\":\"Threshold Crossed: 1 datapoint [1.0 (12/02/19 15:44:00)] was not less than the threshold (1.0).\",\"StateChangeTime\":\"2019-02-12T15:45:24.006+0000\",\"Region\":\"EU (London)\",\"OldStateValue\":\"ALARM\",\"Trigger\":{\"MetricName\":\"DBMigrationRequired\",\"Namespace\":\"LogMetrics\",\"StatisticType\":\"Statistic\",\"Statistic\":\"SUM\",\"Unit\":null,\"Dimensions\":[],\"Period\":60,\"EvaluationPeriods\":1,\"ComparisonOperator\":\"LessThanThreshold\",\"Threshold\":1.0,\"TreatMissingData\":\"- TreatMissingData: NonBreaching\",\"EvaluateLowSampleCountPercentile\":\"\"}}",
"Timestamp": "2019-02-12T15:45:24.091Z",
"SignatureVersion": "1",
"Signature": "WMYdVRN7ECNXMWZ0faRDD4fSfALW5MISB6O//LMd/LeSQYNQ/1eKYEE0PM1SHcH+73T/f/eVHbID/F203VZaGECQTD4LVA4B0DGAEY39LVbWdPTCHIDC6QCBV5ScGFZcROBXMe3UBWWMQAVTSWTE0eP526BFUTecaDFM4b9HMT4NEHWa4A2TA7d888JaVKKdSVNTd4bGS6Q2XFG1MOb652BRAHdARO7A6//2/47JZ5COM6LR0/V7TcOYCBZ20CRF6L5XLU46YYL3I1PNGKbEC1PIeVDVJVPcA17NfUbFXWYBX8LHfM4O7ZbGAPaGffDYLFWM6TX1Y6fQ01OSMc21OdUGV6HQR01e%==",
"SigningCertUrl": "https://sns.eu-west-2.amazonaws.com/SimpleNotificationService-7dd85a2b76adaa8dd603b7a0c9150589.pem",
"UnsubscribeUrl": "https://sns.eu-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-2:735598076380:service-updates:d29b4e2c-6840-9c4e-ceac-17128efcc337",
"MessageAttributes": {}
}
}
]
}
),
(
{
"AlarmName": "Example",
"AlarmDescription": "Example alarm description.",
"AWSAccountId": "000000000000",
"NewStateValue": "ALARM",
"NewStateReason": "Threshold Crossed",
"StateChangeTime": "2017-01-12T16:30:42.236+0000",
"Region": "EU - Ireland",
"OldStateValue": "OK"
}
)
)


@pytest.fixture(scope='module', autouse=True)
def check_environment_variables():
required_environment_variables = ("SLACK_CHANNEL", "SLACK_EMOJI", "SLACK_USERNAME", "SLACK_WEBHOOK_URL")
missing_environment_variables = []
for k in required_environment_variables:
if k not in environ:
missing_environment_variables.append(k)

if len(missing_environment_variables) > 0:
pytest.exit('Missing environment variables: {}'.format(", ".join(missing_environment_variables)))


@pytest.mark.parametrize("event", events)
def test_lambda_handler(event):
if 'Records' in event:
response = notify_slack.lambda_handler(event, 'self-context')

else:
response = notify_slack.notify_slack(event, 'eu-west-1')

response = loads(response)
assert response['code'] == 200
8 changes: 8 additions & 0 deletions functions/pytest.ini.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[pytest]
addopts = --disable-pytest-warnings
env =
SLACK_CHANNEL=slack_testing_sandbox
SLACK_EMOJI=:aws:
SLACK_USERNAME=notify_slack_test
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBOOK/URL

4 changes: 4 additions & 0 deletions functions/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
boto3

pytest
pytest-env
1 change: 1 addition & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ resource "aws_lambda_function" "notify_slack" {
SLACK_CHANNEL = "${var.slack_channel}"
SLACK_USERNAME = "${var.slack_username}"
SLACK_EMOJI = "${var.slack_emoji}"
LOG_EVENTS = "${var.log_events ? "True" : "False"}"
}
}

Expand Down
5 changes: 5 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ variable "kms_key_arn" {
default = ""
}

variable "log_events" {
description = "Boolean flag to enabled/disable logging of incoming events"
default = false
}

locals {
module_relpath = "${substr(path.module, length(path.cwd) + 1, -1)}"
}

0 comments on commit d935666

Please sign in to comment.