Skip to content

Commit

Permalink
feat(DMVP-2705): Create Jira ticket
Browse files Browse the repository at this point in the history
  • Loading branch information
Julia Aghamyan committed Oct 26, 2023
1 parent acca4a6 commit c0e1603
Show file tree
Hide file tree
Showing 14 changed files with 635 additions and 280 deletions.
3 changes: 2 additions & 1 deletion modules/cloudwatch-alarm-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module "monitoring_cloudwatch_alarm_actions" {
|------|--------|---------|
| <a name="module_dead_letter_queue"></a> [dead\_letter\_queue](#module\_dead\_letter\_queue) | dasmeta/modules/aws//modules/sqs | 1.5.1 |
| <a name="module_fallback-topic"></a> [fallback-topic](#module\_fallback-topic) | dasmeta/sns/aws//modules/topic | 1.1.1 |
| <a name="module_notify_jira"></a> [notify\_jira](#module\_notify\_jira) | ./modules/lambda-subscription | n/a |
| <a name="module_notify_servicenow"></a> [notify\_servicenow](#module\_notify\_servicenow) | ./modules/lambda-subscription | n/a |
| <a name="module_notify_slack"></a> [notify\_slack](#module\_notify\_slack) | terraform-aws-modules/notify-slack/aws | 5.4.1 |
| <a name="module_notify_teams"></a> [notify\_teams](#module\_notify\_teams) | ./modules/lambda-subscription | n/a |
Expand All @@ -52,7 +53,7 @@ module "monitoring_cloudwatch_alarm_actions" {
| <a name="input_fallback_email_addresses"></a> [fallback\_email\_addresses](#input\_fallback\_email\_addresses) | List of fallback email addresses to send notification when lambda failed | `list(string)` | `[]` | no |
| <a name="input_fallback_phone_numbers"></a> [fallback\_phone\_numbers](#input\_fallback\_phone\_numbers) | List of international formatted phone number to send notification when lambda failed | `list(string)` | `[]` | no |
| <a name="input_fallback_web_endpoints"></a> [fallback\_web\_endpoints](#input\_fallback\_web\_endpoints) | List of web webhooks endpoints (like opsgenie) to send notification when lambda failed | `list(string)` | `[]` | no |
| <a name="input_jira_config"></a> [jira\_config](#input\_jira\_config) | Lambda create Jira ticket for every alarm | <pre>object({<br> enable = bool,<br> url = string,<br> key = string,<br> user_username = string,<br> user_api_token = string<br> })</pre> | <pre>{<br> "enable": false,<br> "key": null,<br> "url": null,<br> "user_api_token": null,<br> "user_username": null<br>}</pre> | no |
| <a name="input_jira_config"></a> [jira\_config](#input\_jira\_config) | Lambda create Jira ticket for every alarm | <pre>list(object({<br> url = string,<br> key = string,<br> user_username = string,<br> user_api_token = string<br> }))</pre> | `[]` | no |
| <a name="input_lambda_failed_alert"></a> [lambda\_failed\_alert](#input\_lambda\_failed\_alert) | Alert for lambda failed | `any` | <pre>{<br> "equation": "gte",<br> "period": 60,<br> "statistic": "sum",<br> "threshold": 1<br>}</pre> | no |
| <a name="input_log_group_retention_days"></a> [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 |
| <a name="input_log_level"></a> [log\_level](#input\_log\_level) | log level for python code | `string` | `"INFO"` | no |
Expand Down
41 changes: 33 additions & 8 deletions modules/cloudwatch-alarm-actions/lambda-subsciptions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit c0e1603

Please sign in to comment.