From c0e16031f5b7dce3eb757ffb38a57851bce0bbed Mon Sep 17 00:00:00 2001 From: Julia Aghamyan Date: Thu, 26 Oct 2023 11:43:45 +0400 Subject: [PATCH] feat(DMVP-2705): Create Jira ticket --- modules/cloudwatch-alarm-actions/README.md | 3 +- .../lambda-subsciptions.tf | 41 ++- .../src/jira/event_handler.py | 219 ++++++++++++++++ .../lambda-subscription/src/jira/lambda.py | 68 +++++ .../src/teams/event_handler.py | 219 ++++++++++++++++ .../lambda-subscription/src/teams/lambda.py | 242 +----------------- .../tests/all-subscriptions/1-example.tf | 10 + .../tests/jira/0-setup.tf | 16 ++ .../tests/jira/1-example.tf | 18 ++ .../tests/jira/2-assert.tf | 9 + .../tests/jira/README.md | 35 +++ .../tests/teams/1-example.tf | 21 -- .../tests/teams/README.md | 1 - modules/cloudwatch-alarm-actions/variables.tf | 13 +- 14 files changed, 635 insertions(+), 280 deletions(-) create mode 100644 modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/event_handler.py create mode 100644 modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/lambda.py create mode 100644 modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/event_handler.py create mode 100644 modules/cloudwatch-alarm-actions/tests/jira/0-setup.tf create mode 100644 modules/cloudwatch-alarm-actions/tests/jira/1-example.tf create mode 100644 modules/cloudwatch-alarm-actions/tests/jira/2-assert.tf create mode 100644 modules/cloudwatch-alarm-actions/tests/jira/README.md diff --git a/modules/cloudwatch-alarm-actions/README.md b/modules/cloudwatch-alarm-actions/README.md index 304395f..507cb70 100644 --- a/modules/cloudwatch-alarm-actions/README.md +++ b/modules/cloudwatch-alarm-actions/README.md @@ -30,6 +30,7 @@ module "monitoring_cloudwatch_alarm_actions" { |------|--------|---------| | [dead\_letter\_queue](#module\_dead\_letter\_queue) | dasmeta/modules/aws//modules/sqs | 1.5.1 | | [fallback-topic](#module\_fallback-topic) | dasmeta/sns/aws//modules/topic | 1.1.1 | +| [notify\_jira](#module\_notify\_jira) | ./modules/lambda-subscription | n/a | | [notify\_servicenow](#module\_notify\_servicenow) | ./modules/lambda-subscription | n/a | | [notify\_slack](#module\_notify\_slack) | terraform-aws-modules/notify-slack/aws | 5.4.1 | | [notify\_teams](#module\_notify\_teams) | ./modules/lambda-subscription | n/a | @@ -52,7 +53,7 @@ module "monitoring_cloudwatch_alarm_actions" { | [fallback\_email\_addresses](#input\_fallback\_email\_addresses) | List of fallback email addresses to send notification when lambda failed | `list(string)` | `[]` | no | | [fallback\_phone\_numbers](#input\_fallback\_phone\_numbers) | List of international formatted phone number to send notification when lambda failed | `list(string)` | `[]` | no | | [fallback\_web\_endpoints](#input\_fallback\_web\_endpoints) | List of web webhooks endpoints (like opsgenie) to send notification when lambda failed | `list(string)` | `[]` | no | -| [jira\_config](#input\_jira\_config) | Lambda create Jira ticket for every alarm |
object({
enable = bool,
url = string,
key = string,
user_username = string,
user_api_token = string
})
|
{
"enable": false,
"key": null,
"url": null,
"user_api_token": null,
"user_username": null
}
| no | +| [jira\_config](#input\_jira\_config) | Lambda create Jira ticket for every alarm |
list(object({
url = string,
key = string,
user_username = string,
user_api_token = string
}))
| `[]` | no | | [lambda\_failed\_alert](#input\_lambda\_failed\_alert) | Alert for lambda failed | `any` |
{
"equation": "gte",
"period": 60,
"statistic": "sum",
"threshold": 1
}
| no | | [log\_group\_retention\_days](#input\_log\_group\_retention\_days) | The count of days that cloudwatch log group will keep each log item and then will cleanup automatically | `number` | `7` | no | | [log\_level](#input\_log\_level) | log level for python code | `string` | `"INFO"` | no | diff --git a/modules/cloudwatch-alarm-actions/lambda-subsciptions.tf b/modules/cloudwatch-alarm-actions/lambda-subsciptions.tf index 846b2f7..02495bc 100644 --- a/modules/cloudwatch-alarm-actions/lambda-subsciptions.tf +++ b/modules/cloudwatch-alarm-actions/lambda-subsciptions.tf @@ -70,14 +70,39 @@ module "notify_teams" { type = "teams" timeout = 10 environment_variables = { - WEBHOOK_URL = each.value - REGION = data.aws_region.current.name - LOG_LEVEL = var.log_level - CREATE_JIRA_TICKET = var.jira_config.enable - JIRA_URL = var.jira_config.url - JIRA_KEY = var.jira_config.key - JIRA_PASSWORD = var.jira_config.user_api_token - JIRA_USERNAME = var.jira_config.user_username + WEBHOOK_URL = each.value + REGION = data.aws_region.current.name + LOG_LEVEL = var.log_level + } + + recreate_missing_package = var.recreate_missing_package + log_group_retention_days = var.log_group_retention_days + dead_letter_queue_arn = try(module.dead_letter_queue[0].queue_arn, null) + attach_dead_letter_policy = var.enable_dead_letter_queue + + depends_on = [ + module.topic # TODO: seems there is no need on this dependency, but without this it fails on getting topic by name in underlying subscription module, please check and get right solution of this + ] +} + +module "notify_jira" { + source = "./modules/lambda-subscription" + + for_each = { for jira in var.jira_config : jira.url => jira } + + # sns/subscription configs + sns_topic_name = module.topic.name + fallback_sns_topic_name = module.fallback-topic.name + + # lambda configs + uniq_id = each.value.url + type = "jira" + timeout = 10 + environment_variables = { + JIRA_URL = each.value.url + JIRA_KEY = each.value.key + JIRA_PASSWORD = each.value.user_api_token + JIRA_USERNAME = each.value.user_username } recreate_missing_package = var.recreate_missing_package diff --git a/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/event_handler.py b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/event_handler.py new file mode 100644 index 0000000..bcf4ba5 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/event_handler.py @@ -0,0 +1,219 @@ +import logging +import json +import os +import boto3 +import base64 + +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +LOGLEVEL = os.environ.get('LOG_LEVEL', 'INFO').upper() +FALLBACK_SUBJECT = 'AWS Alerts' + +logger = logging.getLogger() +logger.setLevel(getattr(logging, LOGLEVEL)) + +logger.info("log level: {}".format(LOGLEVEL)) + +def event_handler_for_metrics(metrics_body): + + for metrics_this in metrics_body: + if 'MetricStat' in metrics_this: + metric_stat = metrics_this["MetricStat"] + + metric = metric_stat["Metric"] + metric_name = metric["MetricName"] + metric_namespace = metric["Namespace"] + + dimension_string = "" + MetricWidget = {} + + for dimension_object in metric["Dimensions"]: + dimension_string += dimension_object["name"] + \ + "/" + dimension_object["value"] + "/" + + if len(metric["Dimensions"]) == 1: + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["Dimensions"][0]["name"], + metric["Dimensions"][0]["value"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + elif not len(metric["Dimensions"]): + MetricWidget = { + "width": 300, + "height": 200, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["MetricName"], + metric["Namespace"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + else: + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["Dimensions"][1]["name"], + metric["Dimensions"][1]["value"], + metric["Dimensions"][2]["name"], + metric["Dimensions"][2]["value"], + metric["Dimensions"][0]["name"], + metric["Dimensions"][0]["value"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + return MetricWidget,dimension_string,metric_namespace,metric_name + +def event_handler_for_expression(metrics_body): + MetricWidget = {} + metrics_widget = [] + metric_namespace = [] + metric_name = [] + + for metrics_this in metrics_body: + temp_metric = {} + if 'Expression' in metrics_this: + expression = metrics_this["Expression"] + if 'MetricStat' in metrics_this: + metric_stat = metrics_this["MetricStat"] + metric = metric_stat["Metric"] + temp_metric["Namespace"] = metric["Namespace"] + temp_metric["MetricName"] = metric["MetricName"] + temp_metric["id"] = metrics_this["Id"] + temp_metric["Name"] = str(metric["Dimensions"][0]["name"]) + temp_metric["Value"] = str(metric["Dimensions"][0]["value"]) + + metric_name.append(temp_metric["MetricName"]) + metric_namespace.append(temp_metric["Namespace"]) + metrics_widget.append(temp_metric) + metrics = [] + for item in metrics_widget: + temp = [] + temp.append( item["Namespace"]) + temp.append( item["MetricName"]) + temp.append( item["Name"]) + temp.append( item["Value"]) + temp.append( {'id': item["id"]}) + + print(temp) + metrics.append(temp) + + metrics.append([{"expression" : expression}]) + + + MetricWidget = { + "width": 600, + "height": 395, + "metrics": metrics, + "period": 300, + "view": "timeSeries" + } + return MetricWidget,expression,metric_namespace,metric_name + +def event_handler(event, context): + """send message via teams""" + + print("Event",event) + print("Context",context) + + url = "https://" + os.environ['REGION'] + ".console.aws.amazon.com/cloudwatch/home?region=" + \ + os.environ['REGION'] + "#alarmsV2:?~(alarmStateFilter~'ALARM)t" + + logger.debug("Event: {}".format(event)) + message = str(event['Records'][0]['Sns']['Message']) + subject = guess_subject(event) + logger.debug("Event:" + str(event)) + logger.debug("Message: " + str(subject)) + logger.debug("Message: " + str(message)) + + # Json handle and cut info + json_body = json.loads(event["Records"][0]["Sns"]["Message"]) + # json_body = event["Records"][0]["Sns"]["Message"] #TODO: can be removed. This was used to debug lambda on fly + trigger_body = json_body["Trigger"] + aws_account = json_body["AWSAccountId"] + aws_alarmdescription = json_body["AlarmDescription"] + dimension_string = "" + + if "Metrics" in trigger_body: + metrics_body = trigger_body["Metrics"] + logger.debug("> Alarm Metrics Body Value", metrics_body) + print("metrics_body: ",metrics_body) + # processing one single metric + # TODO: we need to manage multiple metrics processing + # for metrics_this in metrics_body: + # try: + # metric_stat = metrics_this["MetricStat"] + + # except KeyError: + # logger.debug("MetricStat is missing in this metric block") + for metric_this in metrics_body: + if metric_this["ReturnData"] == True: + if "Expression" in metric_this: + MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_expression(metrics_body) + alert_type = "Expression" + else: + alert_type = "Metric" + MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_metrics(metrics_body) + + else: + for dimension_object in trigger_body["Dimensions"]: + dimension_string += dimension_object["name"] + \ + "/" + dimension_object["value"] + "/" + + metric_name = trigger_body["MetricName"] + metric_namespace = trigger_body["Namespace"] + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric_namespace, + metric_name, + trigger_body["Dimensions"][0]["name"], + trigger_body["Dimensions"][0]["value"], + { + "stat": "Minimum" + } + ] + ], + "period": 60, + "view": "timeSeries", + } + + # Get Cloudwatch metric widget, encoded base64 + cloudwatch = boto3.client('cloudwatch', region_name=os.environ['REGION']) + response = cloudwatch.get_metric_widget_image( + MetricWidget=json.dumps(MetricWidget)) + encoded_data = base64.b64encode( + response["MetricWidgetImage"]).decode('utf-8') + image = 'data:image/png;base64, {}'.format(encoded_data) + + return alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url diff --git a/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/lambda.py b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/lambda.py new file mode 100644 index 0000000..4bc21b2 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/jira/lambda.py @@ -0,0 +1,68 @@ +import os +import requests +from event_handler import event_handler + +def create_jira_ticket(summary,description): + # Jira API URL and authentication + url = os.environ['JIRA_URL'] + username = os.environ['JIRA_USERNAME'] + password = os.environ['JIRA_PASSWORD'] + issue_data = { + "fields": { + "project": {"key": os.environ['JIRA_KEY']}, + "summary": summary, + "description": description, + "issuetype": {"name": "Task"}, + "labels": ["DevOps"] + } + } + + response = requests.post(url, json=issue_data, auth=(username, password)) + if response.status_code == 201: + print("Issue created successfully. Issue key:", response.json()['key']) + else: + print("Failed to create issue. Status code:", response.status_code) + print("Response content:", response.content) + + +def handler(event, context): + alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url = event_handler(event,context) + + if alert_type == "Expression": + all_data = [ + { + "title": "AccountId", + "value": aws_account + }, { + "title": "Expression", + "value": dimension_string + },{ + "title": "Metrics", + "value": str(metric_name) + },{ + "title": "Namespaces", + "value": str(metric_namespace) + } + ] + else: + all_data = [ + { + "title": "AccountId", + "value": aws_account + }, { + "title": "Dimensions", + "value": dimension_string + }, { + "title": "Metric", + "value": metric_namespace + "/" + metric_name + } + ] + + ok_check = "OK" in subject + if not ok_check: + description = f"\n{aws_alarmdescription}" + description += f"\n h2. Details\n" + description += f"\n".join([f"{item['title']}: {item['value']}" for item in all_data]) + description += f"\nURL: {url}" + print("Create jira ticket") + create_jira_ticket(subject,description) diff --git a/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/event_handler.py b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/event_handler.py new file mode 100644 index 0000000..bcf4ba5 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/event_handler.py @@ -0,0 +1,219 @@ +import logging +import json +import os +import boto3 +import base64 + +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + +LOGLEVEL = os.environ.get('LOG_LEVEL', 'INFO').upper() +FALLBACK_SUBJECT = 'AWS Alerts' + +logger = logging.getLogger() +logger.setLevel(getattr(logging, LOGLEVEL)) + +logger.info("log level: {}".format(LOGLEVEL)) + +def event_handler_for_metrics(metrics_body): + + for metrics_this in metrics_body: + if 'MetricStat' in metrics_this: + metric_stat = metrics_this["MetricStat"] + + metric = metric_stat["Metric"] + metric_name = metric["MetricName"] + metric_namespace = metric["Namespace"] + + dimension_string = "" + MetricWidget = {} + + for dimension_object in metric["Dimensions"]: + dimension_string += dimension_object["name"] + \ + "/" + dimension_object["value"] + "/" + + if len(metric["Dimensions"]) == 1: + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["Dimensions"][0]["name"], + metric["Dimensions"][0]["value"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + elif not len(metric["Dimensions"]): + MetricWidget = { + "width": 300, + "height": 200, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["MetricName"], + metric["Namespace"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + else: + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric["Namespace"], + metric_name, + metric["Dimensions"][1]["name"], + metric["Dimensions"][1]["value"], + metric["Dimensions"][2]["name"], + metric["Dimensions"][2]["value"], + metric["Dimensions"][0]["name"], + metric["Dimensions"][0]["value"], + { + "stat": "Average" + } + ] + ], + "period": 300, + "view": "timeSeries" + } + return MetricWidget,dimension_string,metric_namespace,metric_name + +def event_handler_for_expression(metrics_body): + MetricWidget = {} + metrics_widget = [] + metric_namespace = [] + metric_name = [] + + for metrics_this in metrics_body: + temp_metric = {} + if 'Expression' in metrics_this: + expression = metrics_this["Expression"] + if 'MetricStat' in metrics_this: + metric_stat = metrics_this["MetricStat"] + metric = metric_stat["Metric"] + temp_metric["Namespace"] = metric["Namespace"] + temp_metric["MetricName"] = metric["MetricName"] + temp_metric["id"] = metrics_this["Id"] + temp_metric["Name"] = str(metric["Dimensions"][0]["name"]) + temp_metric["Value"] = str(metric["Dimensions"][0]["value"]) + + metric_name.append(temp_metric["MetricName"]) + metric_namespace.append(temp_metric["Namespace"]) + metrics_widget.append(temp_metric) + metrics = [] + for item in metrics_widget: + temp = [] + temp.append( item["Namespace"]) + temp.append( item["MetricName"]) + temp.append( item["Name"]) + temp.append( item["Value"]) + temp.append( {'id': item["id"]}) + + print(temp) + metrics.append(temp) + + metrics.append([{"expression" : expression}]) + + + MetricWidget = { + "width": 600, + "height": 395, + "metrics": metrics, + "period": 300, + "view": "timeSeries" + } + return MetricWidget,expression,metric_namespace,metric_name + +def event_handler(event, context): + """send message via teams""" + + print("Event",event) + print("Context",context) + + url = "https://" + os.environ['REGION'] + ".console.aws.amazon.com/cloudwatch/home?region=" + \ + os.environ['REGION'] + "#alarmsV2:?~(alarmStateFilter~'ALARM)t" + + logger.debug("Event: {}".format(event)) + message = str(event['Records'][0]['Sns']['Message']) + subject = guess_subject(event) + logger.debug("Event:" + str(event)) + logger.debug("Message: " + str(subject)) + logger.debug("Message: " + str(message)) + + # Json handle and cut info + json_body = json.loads(event["Records"][0]["Sns"]["Message"]) + # json_body = event["Records"][0]["Sns"]["Message"] #TODO: can be removed. This was used to debug lambda on fly + trigger_body = json_body["Trigger"] + aws_account = json_body["AWSAccountId"] + aws_alarmdescription = json_body["AlarmDescription"] + dimension_string = "" + + if "Metrics" in trigger_body: + metrics_body = trigger_body["Metrics"] + logger.debug("> Alarm Metrics Body Value", metrics_body) + print("metrics_body: ",metrics_body) + # processing one single metric + # TODO: we need to manage multiple metrics processing + # for metrics_this in metrics_body: + # try: + # metric_stat = metrics_this["MetricStat"] + + # except KeyError: + # logger.debug("MetricStat is missing in this metric block") + for metric_this in metrics_body: + if metric_this["ReturnData"] == True: + if "Expression" in metric_this: + MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_expression(metrics_body) + alert_type = "Expression" + else: + alert_type = "Metric" + MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_metrics(metrics_body) + + else: + for dimension_object in trigger_body["Dimensions"]: + dimension_string += dimension_object["name"] + \ + "/" + dimension_object["value"] + "/" + + metric_name = trigger_body["MetricName"] + metric_namespace = trigger_body["Namespace"] + MetricWidget = { + "width": 600, + "height": 395, + "metrics": [ + [ + metric_namespace, + metric_name, + trigger_body["Dimensions"][0]["name"], + trigger_body["Dimensions"][0]["value"], + { + "stat": "Minimum" + } + ] + ], + "period": 60, + "view": "timeSeries", + } + + # Get Cloudwatch metric widget, encoded base64 + cloudwatch = boto3.client('cloudwatch', region_name=os.environ['REGION']) + response = cloudwatch.get_metric_widget_image( + MetricWidget=json.dumps(MetricWidget)) + encoded_data = base64.b64encode( + response["MetricWidgetImage"]).decode('utf-8') + image = 'data:image/png;base64, {}'.format(encoded_data) + + return alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url diff --git a/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/lambda.py b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/lambda.py index b72cec2..040b10f 100644 --- a/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/lambda.py +++ b/modules/cloudwatch-alarm-actions/modules/lambda-subscription/src/teams/lambda.py @@ -4,6 +4,7 @@ import boto3 import base64 import requests +from event_handler import event_handler from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError @@ -16,170 +17,7 @@ logger.info("log level: {}".format(LOGLEVEL)) -def create_jira_ticket(summary,description): - # Jira API URL and authentication - url = os.environ['JIRA_URL'] - username = os.environ['JIRA_USERNAME'] - password = os.environ['JIRA_PASSWORD'] - issue_data = { - "fields": { - "project": {"key": os.environ['JIRA_KEY']}, - "summary": summary, - "description": description, - "issuetype": {"name": "Task"}, - "labels": ["DevOps"] - } - } - - response = requests.post(url, json=issue_data, auth=(username, password)) - if response.status_code == 201: - print("Issue created successfully. Issue key:", response.json()['key']) - else: - print("Failed to create issue. Status code:", response.status_code) - print("Response content:", response.content) - -def guess_subject(event): - """ - try to guess a good message subject - - Messages from CloudWatch e.g. concerning ECS don't come with a subject. - """ - - # Just take the provided subject if there is one - subject_from_sns = event['Records'][0]['Sns']['Subject'] - if str(subject_from_sns) != 'None' and len(subject_from_sns) > 0: - logger.info("Message subject from SNS") - return subject_from_sns - else: - logging.info( - "Message subject not included in SNS, using fallback message subject") - return FALLBACK_SUBJECT - -def event_handler_for_metrics(metrics_body): - - for metrics_this in metrics_body: - if 'MetricStat' in metrics_this: - metric_stat = metrics_this["MetricStat"] - - metric = metric_stat["Metric"] - metric_name = metric["MetricName"] - metric_namespace = metric["Namespace"] - - dimension_string = "" - MetricWidget = {} - - for dimension_object in metric["Dimensions"]: - dimension_string += dimension_object["name"] + \ - "/" + dimension_object["value"] + "/" - - if len(metric["Dimensions"]) == 1: - MetricWidget = { - "width": 600, - "height": 395, - "metrics": [ - [ - metric["Namespace"], - metric_name, - metric["Dimensions"][0]["name"], - metric["Dimensions"][0]["value"], - { - "stat": "Average" - } - ] - ], - "period": 300, - "view": "timeSeries" - } - elif not len(metric["Dimensions"]): - MetricWidget = { - "width": 300, - "height": 200, - "metrics": [ - [ - metric["Namespace"], - metric_name, - metric["MetricName"], - metric["Namespace"], - { - "stat": "Average" - } - ] - ], - "period": 300, - "view": "timeSeries" - } - else: - MetricWidget = { - "width": 600, - "height": 395, - "metrics": [ - [ - metric["Namespace"], - metric_name, - metric["Dimensions"][1]["name"], - metric["Dimensions"][1]["value"], - metric["Dimensions"][2]["name"], - metric["Dimensions"][2]["value"], - metric["Dimensions"][0]["name"], - metric["Dimensions"][0]["value"], - { - "stat": "Average" - } - ] - ], - "period": 300, - "view": "timeSeries" - } - return MetricWidget,dimension_string,metric_namespace,metric_name - -def event_handler_for_expression(metrics_body): - MetricWidget = {} - metrics_widget = [] - metric_namespace = [] - metric_name = [] - - for metrics_this in metrics_body: - temp_metric = {} - if 'Expression' in metrics_this: - expression = metrics_this["Expression"] - if 'MetricStat' in metrics_this: - metric_stat = metrics_this["MetricStat"] - metric = metric_stat["Metric"] - temp_metric["Namespace"] = metric["Namespace"] - temp_metric["MetricName"] = metric["MetricName"] - temp_metric["id"] = metrics_this["Id"] - temp_metric["Name"] = str(metric["Dimensions"][0]["name"]) - temp_metric["Value"] = str(metric["Dimensions"][0]["value"]) - - metric_name.append(temp_metric["MetricName"]) - metric_namespace.append(temp_metric["Namespace"]) - metrics_widget.append(temp_metric) - metrics = [] - for item in metrics_widget: - temp = [] - temp.append( item["Namespace"]) - temp.append( item["MetricName"]) - temp.append( item["Name"]) - temp.append( item["Value"]) - temp.append( {'id': item["id"]}) - - print(temp) - metrics.append(temp) - - metrics.append([{"expression" : expression}]) - - - MetricWidget = { - "width": 600, - "height": 395, - "metrics": metrics, - "period": 300, - "view": "timeSeries" - } - return MetricWidget,expression,metric_namespace,metric_name - def payload(alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url): - if alert_type == "Expression": items = [ { @@ -282,84 +120,10 @@ def payload(alert_type,subject,aws_account,aws_alarmdescription,dimension_string return payload def handler(event, context): - """send message via teams""" - - print("Event",event) - print("Context",context) - teams_webhook_url = os.environ['WEBHOOK_URL'] - url = "https://" + os.environ['REGION'] + ".console.aws.amazon.com/cloudwatch/home?region=" + \ - os.environ['REGION'] + "#alarmsV2:?~(alarmStateFilter~'ALARM)t" - - logger.debug("Event: {}".format(event)) - message = str(event['Records'][0]['Sns']['Message']) - subject = guess_subject(event) - logger.debug("Event:" + str(event)) - logger.debug("Message: " + str(subject)) - logger.debug("Message: " + str(message)) - - # Json handle and cut info - json_body = json.loads(event["Records"][0]["Sns"]["Message"]) - # json_body = event["Records"][0]["Sns"]["Message"] #TODO: can be removed. This was used to debug lambda on fly - trigger_body = json_body["Trigger"] - aws_account = json_body["AWSAccountId"] - aws_alarmdescription = json_body["AlarmDescription"] - dimension_string = "" - - if "Metrics" in trigger_body: - metrics_body = trigger_body["Metrics"] - logger.debug("> Alarm Metrics Body Value", metrics_body) - print("metrics_body: ",metrics_body) - # processing one single metric - # TODO: we need to manage multiple metrics processing - # for metrics_this in metrics_body: - # try: - # metric_stat = metrics_this["MetricStat"] - - # except KeyError: - # logger.debug("MetricStat is missing in this metric block") - for metric_this in metrics_body: - if metric_this["ReturnData"] == True: - if "Expression" in metric_this: - MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_expression(metrics_body) - alert_type = "Expression" - else: - alert_type = "Metric" - MetricWidget,dimension_string,metric_namespace,metric_name = event_handler_for_metrics(metrics_body) - - else: - for dimension_object in trigger_body["Dimensions"]: - dimension_string += dimension_object["name"] + \ - "/" + dimension_object["value"] + "/" - - metric_name = trigger_body["MetricName"] - metric_namespace = trigger_body["Namespace"] - MetricWidget = { - "width": 600, - "height": 395, - "metrics": [ - [ - metric_namespace, - metric_name, - trigger_body["Dimensions"][0]["name"], - trigger_body["Dimensions"][0]["value"], - { - "stat": "Minimum" - } - ] - ], - "period": 60, - "view": "timeSeries", - } - - # Get Cloudwatch metric widget, encoded base64 - cloudwatch = boto3.client('cloudwatch', region_name=os.environ['REGION']) - response = cloudwatch.get_metric_widget_image( - MetricWidget=json.dumps(MetricWidget)) - encoded_data = base64.b64encode( - response["MetricWidgetImage"]).decode('utf-8') - image = 'data:image/png;base64, {}'.format(encoded_data) + alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url = event_handler(event,context) payload_data = payload(alert_type,subject,aws_account,aws_alarmdescription,dimension_string,metric_namespace,metric_name,image,url) + teams_webhook_url =os.environ['WEBHOOK_URL'] headers = {'Content-Type': 'application/json'} method = 'POST' diff --git a/modules/cloudwatch-alarm-actions/tests/all-subscriptions/1-example.tf b/modules/cloudwatch-alarm-actions/tests/all-subscriptions/1-example.tf index e8e1ddc..f6fbd76 100644 --- a/modules/cloudwatch-alarm-actions/tests/all-subscriptions/1-example.tf +++ b/modules/cloudwatch-alarm-actions/tests/all-subscriptions/1-example.tf @@ -5,6 +5,9 @@ module "this" { email_addresses = ["alarms-test@example.com"] phone_numbers = ["+000000000"] web_endpoints = [sensitive("https://example.com/")] + + teams_webhooks = ["webhook"] + slack_webhooks = [{ hook_url = "test-slack-hook-url" channel = "test-slack-channel" @@ -17,6 +20,13 @@ module "this" { pass = "test-servicenow-pass" }] + jira_config = { + url = "url" + key = "key" + user_username = "devops" + user_api_token = "devops_user_api_token" + } + fallback_email_addresses = ["test@dasmeta.com"] fallback_phone_numbers = ["+000000000"] fallback_web_endpoints = ["https://example.com/"] diff --git a/modules/cloudwatch-alarm-actions/tests/jira/0-setup.tf b/modules/cloudwatch-alarm-actions/tests/jira/0-setup.tf new file mode 100644 index 0000000..db10e19 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/tests/jira/0-setup.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + + aws = { + source = "hashicorp/aws" + version = "~> 4.33" + } + } +} + +provider "aws" { + region = "eu-central-1" +} diff --git a/modules/cloudwatch-alarm-actions/tests/jira/1-example.tf b/modules/cloudwatch-alarm-actions/tests/jira/1-example.tf new file mode 100644 index 0000000..53fd2ad --- /dev/null +++ b/modules/cloudwatch-alarm-actions/tests/jira/1-example.tf @@ -0,0 +1,18 @@ +module "sns-to-jira" { + source = "../../" + + topic_name = "test-topic" + + fallback_email_addresses = ["devops@dasmeta.com"] + fallback_phone_numbers = ["+374 00 000 000"] + enable_dead_letter_queue = false + recreate_missing_package = false + + teams_webhooks = ["webhook"] + jira_config = [{ + url = "url" + key = "key" + user_username = "devops" + user_api_token = "devops_user_api_token" + }] +} diff --git a/modules/cloudwatch-alarm-actions/tests/jira/2-assert.tf b/modules/cloudwatch-alarm-actions/tests/jira/2-assert.tf new file mode 100644 index 0000000..909a500 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/tests/jira/2-assert.tf @@ -0,0 +1,9 @@ +resource "test_assertions" "dummy" { + component = "monitoring-modules-cloudwatch-alarm-actions" + + equal "scheme" { + description = "As module does not have any output and data just make sure the case runs. Probably can be thrown away." + got = "all good" + want = "all good" + } +} diff --git a/modules/cloudwatch-alarm-actions/tests/jira/README.md b/modules/cloudwatch-alarm-actions/tests/jira/README.md new file mode 100644 index 0000000..f142ea2 --- /dev/null +++ b/modules/cloudwatch-alarm-actions/tests/jira/README.md @@ -0,0 +1,35 @@ +# teams + + +## Requirements + +| Name | Version | +|------|---------| +| [aws](#requirement\_aws) | ~> 4.33 | + +## Providers + +| Name | Version | +|------|---------| +| [test](#provider\_test) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [sns-to-jira](#module\_sns-to-jira) | ../../ | n/a | + +## Resources + +| Name | Type | +|------|------| +| test_assertions.dummy | resource | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + diff --git a/modules/cloudwatch-alarm-actions/tests/teams/1-example.tf b/modules/cloudwatch-alarm-actions/tests/teams/1-example.tf index aac501d..5930234 100644 --- a/modules/cloudwatch-alarm-actions/tests/teams/1-example.tf +++ b/modules/cloudwatch-alarm-actions/tests/teams/1-example.tf @@ -9,24 +9,3 @@ module "sns-to-teams" { enable_dead_letter_queue = false recreate_missing_package = false } - - -module "sns-to-teams-with-jira" { - source = "../../" - - topic_name = "test-topic" - - fallback_email_addresses = ["devops@dasmeta.com"] - fallback_phone_numbers = ["+374 00 000 000"] - teams_webhooks = ["webhook"] - enable_dead_letter_queue = false - recreate_missing_package = false - - jira_config = { - enable = true - url = "url" - key = "key" - user_username = "devops" - user_api_token = "devops_user_api_token" - } -} diff --git a/modules/cloudwatch-alarm-actions/tests/teams/README.md b/modules/cloudwatch-alarm-actions/tests/teams/README.md index 355426f..7a355c3 100644 --- a/modules/cloudwatch-alarm-actions/tests/teams/README.md +++ b/modules/cloudwatch-alarm-actions/tests/teams/README.md @@ -18,7 +18,6 @@ | Name | Source | Version | |------|--------|---------| | [sns-to-teams](#module\_sns-to-teams) | ../../ | n/a | -| [sns-to-teams-with-jira](#module\_sns-to-teams-with-jira) | ../../ | n/a | ## Resources diff --git a/modules/cloudwatch-alarm-actions/variables.tf b/modules/cloudwatch-alarm-actions/variables.tf index b1373dd..ed15b9b 100644 --- a/modules/cloudwatch-alarm-actions/variables.tf +++ b/modules/cloudwatch-alarm-actions/variables.tf @@ -131,20 +131,13 @@ variable "lambda_failed_alert" { } variable "jira_config" { - type = object({ - enable = bool, + type = list(object({ url = string, key = string, user_username = string, user_api_token = string - }) + })) - default = { - enable = false - url = null - key = null - user_username = null - user_api_token = null - } + default = [] description = "Lambda create Jira ticket for every alarm" }