diff --git a/docs/configuration/sinks/jira.rst b/docs/configuration/sinks/jira.rst index e76015b04..d8bd53db2 100644 --- a/docs/configuration/sinks/jira.rst +++ b/docs/configuration/sinks/jira.rst @@ -23,6 +23,15 @@ Prerequisites Optional Settings --------------------------- * ``issue_type`` : [Optional - default: ``Task``] Jira ticket type +* ``priority_mapping`` : [Optional] Maps Robusta severity levels to Jira priorities. Example: + .. code-block:: yaml + + priority_mapping: + HIGH: "High" + MEDIUM: "Medium" + LOW: "Low" + INFO: "Lowest" + * ``dedups`` : [Optional - default: ``fingerprint``] Tickets deduplication parameter. By default, Only one issue per ``fingerprint`` will be created. There can be more than one value to use. Possible values are: fingerprint, cluster_name, title, node, type, source, namespace, creation_date etc * ``project_type_id_override`` : [Optional - default: None] If available, will override the ``project_name`` configuration. Follow these `instructions `__ to get your project id. * ``issue_type_id_override`` : [Optional - default: None] If available, will override the ``issue_type`` configuration. Follow these `instructions `__ to get your issue id. @@ -59,6 +68,11 @@ Configuring the Jira sink assignee: user_id of the assignee(OPTIONAL) epic: epic_id(OPTIONAL) project_name: project_name + priority_mapping: (OPTIONAL) + HIGH: "High" + MEDIUM: "Medium" + LOW: "Low" + INFO: "Lowest" scope: include: - identifier: [CPUThrottlingHigh, KubePodCrashLooping] diff --git a/src/robusta/core/sinks/jira/jira_sink_params.py b/src/robusta/core/sinks/jira/jira_sink_params.py index 3e5a9561c..9824128de 100644 --- a/src/robusta/core/sinks/jira/jira_sink_params.py +++ b/src/robusta/core/sinks/jira/jira_sink_params.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Dict, List, Optional from robusta.core.sinks.sink_base_params import SinkBaseParams from robusta.core.sinks.sink_config import SinkConfigBase @@ -20,6 +20,7 @@ class JiraSinkParams(SinkBaseParams): noReopenResolution: Optional[str] = "" epic: Optional[str] = "" assignee: Optional[str] = "" + priority_mapping: Optional[Dict[str, str]] = None @classmethod diff --git a/src/robusta/integrations/jira/client.py b/src/robusta/integrations/jira/client.py index e53c197fb..0e8e42a90 100644 --- a/src/robusta/integrations/jira/client.py +++ b/src/robusta/integrations/jira/client.py @@ -1,7 +1,8 @@ import logging -from typing import Optional +from typing import Optional, Dict from requests.auth import HTTPBasicAuth +from requests.exceptions import HTTPError from requests_toolbelt import MultipartEncoder from robusta.core.reporting.base import FindingStatus @@ -58,6 +59,24 @@ def __init__(self, jira_params: JiraSinkParams): f"Jira initialized successfully. Project: {self.default_project_id} issue type: {self.default_issue_type_id}" ) + if jira_params.priority_mapping: + if logging.getLogger().getEffectiveLevel() <= logging.DEBUG: + self._validate_priorities(jira_params.priority_mapping) + + def _validate_priorities(self, priority_mapping: Dict[str, str]) -> None: + """Validate that configured priorities exist in Jira""" + endpoint = "priority" + url = self._get_full_jira_url(endpoint) + available_priorities = self._call_jira_api(url) or [] + available_priority_names = {p.get("name") for p in available_priorities} + + for severity, priority in priority_mapping.items(): + if priority not in available_priority_names: + logging.warning( + f"Configured priority '{priority}' for severity '{severity}' " + f"is not available in Jira. Available priorities: {available_priority_names}" + ) + def _get_full_jira_url(self, endpoint: str) -> str: return "/".join([self.params.url, _API_PREFIX, endpoint]) @@ -160,6 +179,23 @@ def _get_default_project_id(self): return default_issue["id"] return None + def _resolve_priority(self, priority_name: str) -> dict: + """Resolve Jira priority: + 1. User configured priority mapping (if defined) + 2. Fallback to current behavior (use priority name as-is) + + Returns: + dict: Priority field in format {"name": str} + """ + # 1. Try user configured priority mapping + if hasattr(self, "params") and self.params.priority_mapping: + for severity, mapped_name in self.params.priority_mapping.items(): + if mapped_name == priority_name: + return {"name": mapped_name} + + # 2. Fallback to current behavior + return {"name": priority_name} + def list_issues(self, search_params: Optional[str] = None): endpoint = "search" search_params = search_params or "" @@ -208,6 +244,13 @@ def comment_issue(self, issue_id, text): def create_issue(self, issue_data, issue_attachments=None): endpoint = "issue" url = self._get_full_jira_url(endpoint) + + # Add priority resolution if it exists + if "priority" in issue_data: + priority_name = issue_data["priority"].get("name") + if priority_name: + issue_data["priority"] = self._resolve_priority(priority_name) + payload = { "update": {}, "fields": { diff --git a/src/robusta/integrations/jira/sender.py b/src/robusta/integrations/jira/sender.py index 42d26a808..4cfcb5a3d 100644 --- a/src/robusta/integrations/jira/sender.py +++ b/src/robusta/integrations/jira/sender.py @@ -18,6 +18,13 @@ from robusta.core.sinks.jira.jira_sink_params import JiraSinkParams from robusta.integrations.jira.client import JiraClient +SEVERITY_JIRA_ID = { + FindingSeverity.HIGH: "Critical", + FindingSeverity.MEDIUM: "Major", + FindingSeverity.LOW: "Minor", + FindingSeverity.INFO: "Minor", +} + SEVERITY_EMOJI_MAP = { FindingSeverity.HIGH: ":red_circle:", FindingSeverity.MEDIUM: ":large_orange_circle:", @@ -30,13 +37,6 @@ FindingSeverity.LOW: "#ffdc06", FindingSeverity.INFO: "#05aa01", } -# Jira priorities, see: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-priorities/#api-group-issue-priorities -SEVERITY_JIRA_ID = { - FindingSeverity.HIGH: "Critical", - FindingSeverity.MEDIUM: "Major", - FindingSeverity.LOW: "Minor", - FindingSeverity.INFO: "Minor", -} STRONG_MARK_REGEX = r"\*{1}[\w|\s\d%!><=\-:;@#$%^&()\.\,\]\[\\\/'\"]+\*{1}" ITALIAN_MARK_REGEX = r"(^|\s+)_{1}[\w|\s\d%!*><=\-:;@#$%^&()\.\,\]\[\\\/'\"]+_{1}(\s+|$)" @@ -237,16 +237,21 @@ def send_finding_to_jira( FindingStatus.RESOLVED if finding.title.startswith("[RESOLVED]") else FindingStatus.FIRING ) - # Default priority is "Major" if not a standard severity is given + # Use user priority mapping if available, otherwise fall back to default severity = SEVERITY_JIRA_ID.get(finding.severity, "Major") + if self.params.priority_mapping: + severity = self.params.priority_mapping.get(finding.severity.name, severity) + + issue_data = { + "description": {"type": "doc", "version": 1, "content": actions + output_blocks}, + "summary": finding.title, + "labels": labels, + "priority": {"name": severity}, + } + # Let client.manage_issue handle the fallback to ID if name fails self.client.manage_issue( - { - "description": {"type": "doc", "version": 1, "content": actions + output_blocks}, - "summary": finding.title, - "labels": labels, - "priority": {"name": severity}, - }, + issue_data, {"status": status, "source": finding.source}, file_blocks, )