Skip to content

Commit 0e5385b

Browse files
authored
Merge pull request #1709 from logit-io/jira-issue
Jira: migrate bumping to enhanced_search_issues (no legacy fallback)
2 parents bdb0673 + 44c071f commit 0e5385b

File tree

4 files changed

+197
-9
lines changed

4 files changed

+197
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
- Update build libraries: pylint, pytest, pytest-cov, pytest-xdist, sphinx, tox - [#1706](https://github.com/jertel/elastalert2/pull/1706) - @nsano-rururu
2424
- Update docs build to use Ubuntu 24.40 and Python 3.13 - [#1708](https://github.com/jertel/elastalert2/pull/1708) - @jertel
2525
- Cleanup unused imports - [#1708](https://github.com/jertel/elastalert2/pull/1708) - @jertel
26+
- **Jira Migration**: Updated for Jira Cloud API deprecation (legacy JQL search endpoints will be removed after
27+
May 1, 2025). ElastAlert 2 now uses the Jira client's `enhanced_search_issues()` method for ticket bumping
28+
behavior, which internally uses the new Jira API endpoints. This change is transparent for users with compatible
29+
Jira library versions (3.10.5+). No configuration changes required.
2630

2731
# 2.26.0
2832

docs/source/alerts.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,12 @@ Example usage::
13771377
``jira_bump_after_inactivity``: If this is set, ElastAlert 2 will only comment on tickets that have been inactive for at least this many days.
13781378
It only applies if ``jira_bump_tickets`` is true. Default is 0 days.
13791379

1380+
.. note::
1381+
**API Migration Notice**: Starting May 1, 2025, Atlassian deprecated the legacy JQL search endpoints. ElastAlert 2
1382+
now uses the Jira client's ``enhanced_search_issues`` method for bumping logic, which automatically uses the new
1383+
Jira API endpoints internally. This change is transparent for users with compatible Jira library versions (3.10.5+).
1384+
No configuration changes required.
1385+
13801386
Arbitrary Jira fields:
13811387

13821388
ElastAlert 2 supports setting any arbitrary Jira field that your Jira issue supports. For example, if you had a custom field, called "Affected User", you can set it by providing that field name in ``snake_case`` prefixed with ``jira_``. These fields can contain primitive strings or arrays of strings. Note that when you create a custom field in your Jira server, internally, the field is represented as ``customfield_1111``. In ElastAlert 2, you may refer to either the public facing name OR the internal representation.

elastalert/alerters/jira.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,26 @@ def find_existing_ticket(self, matches):
262262
if self.bump_not_in_statuses:
263263
jql = '%s and status not in (%s)' % (jql, ','.join(["\"%s\"" % status if ' ' in status else status
264264
for status in self.bump_not_in_statuses]))
265+
266+
# Force use of enhanced_search_issues (new API)
267+
# This method should be available in updated jira library versions
268+
if not hasattr(self.client, 'enhanced_search_issues'):
269+
raise EAException("enhanced_search_issues method not available. Please update your jira library to a version that supports the new Jira API endpoints.")
270+
265271
try:
266-
issues = self.client.search_issues(jql)
272+
issues = self.client.enhanced_search_issues(jql)
273+
274+
# Ensure issues is a list-like object before checking length
275+
if issues and len(issues):
276+
return issues[0]
267277
except JIRAError as e:
268278
elastalert_logger.exception("Error while searching for Jira ticket using jql '%s': %s" % (jql, e))
269279
return None
280+
except Exception as e:
281+
elastalert_logger.exception("Unexpected error while searching for Jira ticket: %s" % e)
282+
return None
270283

271-
if len(issues):
272-
return issues[0]
284+
return None
273285

274286
def comment_on_ticket(self, ticket, match):
275287
text = str(JiraFormattedMatchString(self.rule, match))

tests/alerters/jira_test.py

Lines changed: 172 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from unittest import mock
77

88
from elastalert.alerters.jira import JiraFormattedMatchString, JiraAlerter
9-
from elastalert.util import ts_now
9+
from elastalert.util import ts_now, EAException
1010
from tests.alerts_test import mock_rule
1111

1212

@@ -90,14 +90,15 @@ def test_jira(caplog):
9090
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
9191
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
9292
mock_jira.return_value = mock.Mock()
93-
mock_jira.return_value.search_issues.return_value = []
93+
# Mock enhanced_search_issues method
94+
mock_jira.return_value.enhanced_search_issues.return_value = []
9495
mock_jira.return_value.priorities.return_value = [mock_priority]
9596
mock_jira.return_value.fields.return_value = []
9697

9798
alert = JiraAlerter(rule)
9899
alert.alert([{'test_term': 'test_value', '@timestamp': '2014-10-31T00:00:00'}])
99100

100-
expected.insert(3, mock.call().search_issues(mock.ANY))
101+
expected.insert(3, mock.call().enhanced_search_issues(mock.ANY))
101102
assert mock_jira.mock_calls == expected
102103

103104
# Remove a field if jira_ignore_in_title set
@@ -106,21 +107,24 @@ def test_jira(caplog):
106107
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
107108
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
108109
mock_jira.return_value = mock.Mock()
109-
mock_jira.return_value.search_issues.return_value = []
110+
# Mock enhanced_search_issues method
111+
mock_jira.return_value.enhanced_search_issues.return_value = []
110112
mock_jira.return_value.priorities.return_value = [mock_priority]
111113
mock_jira.return_value.fields.return_value = []
112114

113115
alert = JiraAlerter(rule)
114116
alert.alert([{'test_term': 'test_value', '@timestamp': '2014-10-31T00:00:00'}])
115117

118+
# Check that 'test_value' was removed from the JQL query when jira_ignore_in_title is set
116119
assert 'test_value' not in mock_jira.mock_calls[3][1][0]
117120

118-
# Issue is still created if search_issues throws an exception
121+
# Issue is still created if enhanced_search_issues throws an exception
119122
with mock.patch('elastalert.alerters.jira.JIRA') as mock_jira, \
120123
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
121124
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
122125
mock_jira.return_value = mock.Mock()
123-
mock_jira.return_value.search_issues.side_effect = JIRAError
126+
# Mock enhanced_search_issues to raise an exception
127+
mock_jira.return_value.enhanced_search_issues.side_effect = JIRAError
124128
mock_jira.return_value.priorities.return_value = [mock_priority]
125129
mock_jira.return_value.fields.return_value = []
126130

@@ -143,6 +147,8 @@ def test_jira(caplog):
143147
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
144148
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
145149
mock_jira.return_value = mock.Mock()
150+
# Mock both search methods
151+
mock_jira.return_value.enhanced_search_issues.return_value = [mock_issue]
146152
mock_jira.return_value.search_issues.return_value = [mock_issue]
147153
mock_jira.return_value.priorities.return_value = [mock_priority]
148154
mock_jira.return_value.fields.return_value = []
@@ -159,6 +165,8 @@ def test_jira(caplog):
159165
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
160166
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
161167
mock_jira.return_value = mock.Mock()
168+
# Mock both search methods
169+
mock_jira.return_value.enhanced_search_issues.return_value = [mock_issue]
162170
mock_jira.return_value.search_issues.return_value = [mock_issue]
163171
mock_jira.return_value.priorities.return_value = [mock_priority]
164172
mock_jira.return_value.fields.return_value = []
@@ -194,6 +202,8 @@ def test_jira(caplog):
194202
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
195203
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
196204
mock_jira.return_value = mock.Mock()
205+
# Mock both search methods
206+
mock_jira.return_value.enhanced_search_issues.return_value = [mock_issue]
197207
mock_jira.return_value.search_issues.return_value = [mock_issue]
198208
mock_jira.return_value.fields.return_value = mock_fields
199209
mock_jira.return_value.priorities.return_value = [mock_priority]
@@ -490,3 +500,159 @@ def test_create_subtask(caplog):
490500
assert 'elastalert' == user
491501
assert logging.INFO == level
492502
assert 'pened Jira ticket:' in message
503+
504+
505+
def test_jira_enhanced_search_when_available():
506+
"""Test that enhanced_search_issues is used when available"""
507+
rule = {
508+
'name': 'test alert',
509+
'jira_account_file': 'jirafile',
510+
'type': mock_rule(),
511+
'jira_project': 'testproject',
512+
'jira_priority': 0,
513+
'jira_issuetype': 'testtype',
514+
'jira_server': 'https://test.atlassian.net',
515+
'jira_bump_tickets': True,
516+
'jira_max_age': 30,
517+
'timestamp_field': '@timestamp',
518+
'rule_file': '/tmp/foo.yaml'
519+
}
520+
521+
mock_priority = mock.Mock(id='5')
522+
mock_issue = mock.Mock()
523+
mock_issue.key = 'TEST-123'
524+
525+
with mock.patch('elastalert.alerters.jira.JIRA') as mock_jira, \
526+
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
527+
528+
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
529+
mock_jira.return_value.priorities.return_value = [mock_priority]
530+
mock_jira.return_value.fields.return_value = []
531+
532+
# Mock enhanced_search_issues method
533+
mock_jira.return_value.enhanced_search_issues = mock.Mock(return_value=[mock_issue])
534+
535+
alert = JiraAlerter(rule)
536+
result = alert.find_existing_ticket([{'test': 'value', '@timestamp': '2014-10-31T00:00:00'}])
537+
538+
# Verify enhanced_search_issues was called
539+
assert mock_jira.return_value.enhanced_search_issues.called
540+
call_args = mock_jira.return_value.enhanced_search_issues.call_args
541+
assert 'project=testproject' in call_args[0][0] # First positional argument
542+
543+
# Verify legacy search was NOT called
544+
assert not hasattr(mock_jira.return_value, 'search_issues') or not mock_jira.return_value.search_issues.called
545+
546+
# Verify the result
547+
assert result is not None
548+
assert result.key == 'TEST-123'
549+
550+
551+
def test_jira_enhanced_search_not_available_error():
552+
"""Test that Jira alerter raises error when enhanced_search_issues is not available"""
553+
rule = {
554+
'name': 'test alert',
555+
'jira_account_file': 'jirafile',
556+
'type': mock_rule(),
557+
'jira_project': 'testproject',
558+
'jira_priority': 0,
559+
'jira_issuetype': 'testtype',
560+
'jira_server': 'https://test.atlassian.net',
561+
'jira_bump_tickets': True,
562+
'jira_max_age': 30,
563+
'timestamp_field': '@timestamp',
564+
'rule_file': '/tmp/foo.yaml'
565+
}
566+
567+
mock_priority = mock.Mock(id='5')
568+
569+
with mock.patch('elastalert.alerters.jira.JIRA') as mock_jira, \
570+
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
571+
572+
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
573+
mock_jira.return_value.priorities.return_value = [mock_priority]
574+
mock_jira.return_value.fields.return_value = []
575+
576+
# Simulate enhanced_search_issues not being available
577+
if hasattr(mock_jira.return_value, 'enhanced_search_issues'):
578+
delattr(mock_jira.return_value, 'enhanced_search_issues')
579+
580+
alert = JiraAlerter(rule)
581+
582+
# Should raise EAException when enhanced_search_issues is not available
583+
with pytest.raises(EAException, match="enhanced_search_issues method not available"):
584+
alert.find_existing_ticket([{'test': 'value', '@timestamp': '2014-10-31T00:00:00'}])
585+
586+
587+
def test_jira_error_handling():
588+
"""Test error handling when search methods throw exceptions"""
589+
rule = {
590+
'name': 'test alert',
591+
'jira_account_file': 'jirafile',
592+
'type': mock_rule(),
593+
'jira_project': 'testproject',
594+
'jira_priority': 0,
595+
'jira_issuetype': 'testtype',
596+
'jira_server': 'https://test.atlassian.net',
597+
'jira_bump_tickets': True,
598+
'jira_max_age': 30,
599+
'timestamp_field': '@timestamp',
600+
'rule_file': '/tmp/foo.yaml'
601+
}
602+
603+
mock_priority = mock.Mock(id='5')
604+
605+
with mock.patch('elastalert.alerters.jira.JIRA') as mock_jira, \
606+
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
607+
608+
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
609+
mock_jira.return_value.priorities.return_value = [mock_priority]
610+
mock_jira.return_value.fields.return_value = []
611+
612+
# Mock enhanced_search_issues to raise an exception
613+
mock_jira.return_value.enhanced_search_issues = mock.Mock(side_effect=JIRAError("API Error"))
614+
615+
alert = JiraAlerter(rule)
616+
result = alert.find_existing_ticket([{'test': 'value', '@timestamp': '2014-10-31T00:00:00'}])
617+
618+
# Verify enhanced_search_issues was called but returned None due to error
619+
assert mock_jira.return_value.enhanced_search_issues.called
620+
assert result is None
621+
622+
623+
def test_jira_no_results():
624+
"""Test that search returns None when no tickets are found"""
625+
rule = {
626+
'name': 'test alert',
627+
'jira_account_file': 'jirafile',
628+
'type': mock_rule(),
629+
'jira_project': 'testproject',
630+
'jira_priority': 0,
631+
'jira_issuetype': 'testtype',
632+
'jira_server': 'https://test.atlassian.net',
633+
'jira_bump_tickets': True,
634+
'jira_max_age': 30,
635+
'timestamp_field': '@timestamp',
636+
'rule_file': '/tmp/foo.yaml'
637+
}
638+
639+
mock_priority = mock.Mock(id='5')
640+
641+
with mock.patch('elastalert.alerters.jira.JIRA') as mock_jira, \
642+
mock.patch('elastalert.alerters.jira.read_yaml') as mock_open:
643+
644+
mock_open.return_value = {'user': 'jirauser', 'password': 'jirapassword'}
645+
mock_jira.return_value.priorities.return_value = [mock_priority]
646+
mock_jira.return_value.fields.return_value = []
647+
648+
# Mock enhanced_search_issues to return empty list
649+
mock_jira.return_value.enhanced_search_issues = mock.Mock(return_value=[])
650+
651+
alert = JiraAlerter(rule)
652+
result = alert.find_existing_ticket([{'test': 'value', '@timestamp': '2014-10-31T00:00:00'}])
653+
654+
# Verify enhanced_search_issues was called
655+
assert mock_jira.return_value.enhanced_search_issues.called
656+
657+
# Verify no result was returned
658+
assert result is None

0 commit comments

Comments
 (0)