Setting Jira Priority and Labels with OpsGenie Edge Connector

Insert Movie Pun

The default OpsGenie integration with Jira Service Management has a puzzling omission when it comes to their OpsGenie Edge Connector integration - it doesn't send Priority or Tags. This means that tags won't pass through as labels, and the priority will be at the default.

My ideal is that the Jira priority maps directly to the OpsGenie priority, especially when SLAs are set against those priorities. Tags and labels power automation, reporting, and documentation in our environment - so passing them through is a must.

Now obviously Atlassian is pivoting to retire their server products and offer "cloud only" by 2024, while working to more closely integrate OpsGenie with their Jira Service Management offering, but for those still using an on-premise server and OpsGenie, this might be of help. 

I haven't had cause to do a lot with Python before, but I modified the actionExecutor.py to perform a query against the OpsGenie Alerts API before creating the Jira ticket. It retrieves the priority and tags, maps OpsGenie priorities to the standard Jira priorities (eg. P1 = Highest), and adds the tags as Jira labels.

I also add a static "OpsGenie" label, because I have some Jira Automation rules that work against that label to fire off webhooks back to OpsGenie. 

In our environment, "P5" priorities don't exist - so I map a P5 to Low rather than Lowest. That's relatively trivial to change in the script if you need to.

The amended script is below, or you can grab it from my Github fork.


import argparse
import json
import logging
import sys

import requests
from requests.auth import HTTPBasicAuth

parser = argparse.ArgumentParser()
parser.add_argument('-payload', '--queuePayload', help='Payload from queue', required=True)
parser.add_argument('-apiKey', '--apiKey', help='The apiKey of the integration', required=True)
parser.add_argument('-opsgenieUrl', '--opsgenieUrl', help='The url', required=True)
parser.add_argument('-logLevel', '--logLevel', help='Level of log', required=True)
parser.add_argument('-url', '--url', help='URL', required=False)
parser.add_argument('-username', '--username', help='Username', required=False)
parser.add_argument('-password', '--password', help='Password', required=False)
parser.add_argument('-key', '--key', help='Project key', required=False)
parser.add_argument('-issueTypeName', '--issueTypeName', help='Issue Type', required=False)
args = vars(parser.parse_args())

logging.basicConfig(stream=sys.stdout, level=args['logLevel'])


def parse_field(key, mandatory):
    variable = queue_message.get(key)
    if not variable:
        variable = args.get(key)
    if mandatory and not variable:
        logging.error(LOG_PREFIX + " Skipping action, Mandatory conf item '" + key +
                      "' is missing. Check your configuration file.")
        raise ValueError(LOG_PREFIX + " Skipping action, Mandatory conf item '" + key +
                         "' is missing. Check your configuration file.")
    return variable


def parse_timeout():
    parsed_timeout = args.get('http.timeout')
    if not parsed_timeout:
        return 30000
    return int(parsed_timeout)


def get_transition_id(request_headers, jira_url, transition_name, token):
    transition_id = str()
    response = requests.get(jira_url, None, headers=request_headers, auth=token, timeout=timeout)
    body = response.json()
    if body != {} and response.status_code < 299:
        transition_list = body["transitions"]
        for transition in transition_list:
            to = transition['to']
            if transition_name == to['name']:
                transition_id = transition['id']
        logging.info(LOG_PREFIX + " Successfully executed at Jira Service Desk")
        logging.debug(
            LOG_PREFIX + " Jira Service Desk response: " + str(response.status_code) + " " + str(response.content))
    else:
        logging.error(
            LOG_PREFIX + " Could not execute at Jira Service Desk; response: " + str(
                response.content) + " status code: " + str(response.status_code))
    if transition_id:
        return transition_id
    else:
        logging.debug(LOG_PREFIX + " Transition id is empty")

def get_alert(opsgenieUrl, api_Key, alert_id):
    headers = {
        "Content-Type": "application/json",
        "Accept-Language": "application/json",
        "Authorization": "GenieKey " + args.get('apiKey')
    }
    
    alert_api_url = opsgenieUrl + "/v2/alerts/" + alert_id + '?identifierType=id'
    alert_response = requests.get(alert_api_url, headers=headers, timeout=timeout)
    if alert_response.status_code < 299:
        logging.info(LOG_PREFIX + " Successfully requested alert from Opsgenie")
        logging.debug(
            LOG_PREFIX + "OpsGenie response: " + str(alert_response.content) + " " + str(
                alert_response.status_code))
        return alert_response.json()
    else:
        logging.warning(
            LOG_PREFIX + " Could not execute at Opsgenie; response: " + str(
                alert_response.content) + " status code: " + str(alert_response.status_code))


def get_tags(alert):
        return alert['data']['tags']

def get_priority(alert):
    priorities = {"P1":"Highest","P2":"High","P3":"Medium","P4":"Low","P5":"Low"}
    priorityMap = "{" + alert["data"]["priority"] + "}"
    logging.debug(priorityMap + ' result: ' + priorityMap.format_map(priorities))
    return priorityMap.format_map(priorities)

def main():
    global LOG_PREFIX
    global queue_message
    global timeout

    queue_message_string = args['queuePayload']
    queue_message = json.loads(queue_message_string)

    logging.debug(str(queue_message))

    alert_id = queue_message["alert"]["alertId"]
    mapped_action = queue_message["mappedActionV2"]["name"]

    LOG_PREFIX = "[" + mapped_action + "]"
    logging.info("Will execute " + mapped_action + " for alertId " + alert_id)

    timeout = parse_timeout()
    url = parse_field('url', True)
    username = parse_field('username', True)
    password = parse_field('password', True)
    project_key = parse_field('key', False)
    issue_type_name = parse_field('issueTypeName', False)

    issue_key = queue_message.get("IssueKey")

    logging.debug("Url: " + str(url))
    logging.debug("Username: " + str(username))
    logging.debug("Project Key: " + str(project_key))
    logging.debug("Issue Type: " + str(issue_type_name))
    logging.debug("Issue Key: " + str(issue_key))

    content_params = dict()

    token = HTTPBasicAuth(username, password)
    headers = {
        "Content-Type": "application/json",
        "Accept-Language": "application/json"
    }

    result_url = url + "/rest/api/2/issue"

    if mapped_action == "addComment":
        content_params = {
            "body": queue_message.get('body')
        }
        result_url += "/" + str(issue_key) + "/comment"
    elif mapped_action == "createIssue":
        toLabel = queue_message.get("alias")
        alert = get_alert(args.get('opsgenieUrl'), args.get('apiKey'), alert_id)
        priority = get_priority(alert)
        labels = get_tags(alert)
        labels.append(toLabel)
        labels.append("OpsGenie")
        content_params = {
            "fields": {
                "project": {
                    "key": project_key
                },
                "issuetype": {
                    "name": issue_type_name
                },
                "summary": queue_message.get("summary"),
                "description": queue_message.get("description"),
                "priority": { "name": priority },
                "labels": labels
            }
        }
    elif mapped_action == "resolveIssue":
        result_url += "/" + str(issue_key) + "/transitions"
        content_params = {
            "transition": {
                "id": get_transition_id(headers, result_url, "Resolved", token)
            },
            "fields": {
                "resolution": {
                    "name": "Done"
                }
            }
        }

    logging.debug(str(content_params))    
    response = requests.post(result_url, data=json.dumps(content_params), headers=headers, auth=token, timeout=timeout)
    if response.status_code < 299:
        logging.info("Successfully executed at Jira Service Desk")
        if mapped_action == "createIssue":
            if response.json():
                issue_key_from_response = response.json()['key']
                if issue_key_from_response:
                    alert_api_url = args.get('opsgenieUrl') + "/v2/alerts/" + alert_id + "/details"
                    content = {
                        "details":
                            {
                                "issueKey": issue_key_from_response
                            }
                    }
                    headers = {
                        "Content-Type": "application/json",
                        "Accept-Language": "application/json",
                        "Authorization": "GenieKey " + args.get('apiKey')
                    }
                    alert_response = requests.post(alert_api_url,
                                                   data=json.dumps(content), headers=headers, timeout=timeout)
                    if alert_response.status_code < 299:
                        logging.info(LOG_PREFIX + " Successfully sent to Opsgenie")
                        logging.debug(
                            LOG_PREFIX + " Jira Service Desk response: " + str(alert_response.content) + " " + str(
                                alert_response.status_code))
                    else:
                        logging.warning(
                            LOG_PREFIX + " Could not execute at Opsgenie; response: " + str(
                                alert_response.content) + " status code: " + str(alert_response.status_code))
            else:
                logging.warning(
                    LOG_PREFIX + " Jira Service Desk response is empty")
    else:
        logging.warning(
            LOG_PREFIX + " Could not execute at Jira Service Desk; response: " + str(
                response.content) + " status code: " + str(response.status_code))


if __name__ == '__main__':
    main()

Comments

You may also like:

Playing nicely with others in the Seq ecosystem

You would realise by now that I'm quite a fan of Seq. It's hard not to be, when you can download a free single user license and get started with a trial, a POC, or designing your monitoring and infrastructure. The growth in open source apps for Seq over the...

Using Handlebars templates with Seq.App.Atlassian.Jira and Seq.App.OpsGenie

One of the really useful things with Seq.App.Opsgenie was that Nick Blumhardt had integrated Handlebars templates to the app, using Handlebars.NET. I liked it so much that, when I contributed to Ali Özgür's excellent Seq.App.Atlassian.Jira project, I carried it over. There's a few oddities in the JIRA REST API, and...