Skip to content

Commit ff16faa

Browse files
authored
Adds / modifies logging for a few Jira service methods (#767)
Adds & modifies debug logging for * get_issue * create_issue * update_issue_resolution It also reuses the Atlassian library's client logger in our patched `raise_for_status` method, rather than the `jbi.services.jira` logger. This helps keep the concerns of different loggers separate.
1 parent 3ccc9f5 commit ff16faa

File tree

4 files changed

+282
-30
lines changed

4 files changed

+282
-30
lines changed

jbi/services/jira.py

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import requests
1717
from atlassian import Jira
1818
from atlassian import errors as atlassian_errors
19+
from atlassian.rest_client import log as atlassian_logger
1920
from requests import exceptions as requests_exceptions
2021

2122
from jbi import Operation, environment
@@ -81,7 +82,7 @@ def raise_for_status(self, *args, **kwargs):
8182
except requests.HTTPError as exc:
8283
request = exc.request
8384
response = exc.response
84-
logger.error(
85+
atlassian_logger.error(
8586
"HTTP: %s %s -> %s %s",
8687
request.method,
8788
request.path_url,
@@ -235,8 +236,15 @@ def _all_project_issue_types_exist(self, actions: Actions):
235236

236237
def get_issue(self, context: ActionContext, issue_key):
237238
"""Return the Jira issue fields or `None` if not found."""
239+
logger.debug("Getting issue %s", issue_key, extra=context.model_dump())
238240
try:
239-
return self.client.get_issue(issue_key)
241+
response = self.client.get_issue(issue_key)
242+
logger.debug(
243+
"Received issue %s",
244+
issue_key,
245+
extra={"response": response, **context.model_dump()},
246+
)
247+
return response
240248
except requests_exceptions.HTTPError as exc:
241249
if getattr(exc.response, "status_code", None) != 404:
242250
raise
@@ -253,33 +261,43 @@ def create_jira_issue(
253261
):
254262
"""Create a Jira issue with basic fields in the project and return its key."""
255263
bug = context.bug
256-
logger.debug(
257-
"Create new Jira issue for Bug %s",
258-
bug.id,
259-
extra=context.model_dump(),
260-
)
261264
fields: dict[str, Any] = {
262265
"summary": bug.summary,
263266
"issuetype": {"name": issue_type},
264267
"description": description[:JIRA_DESCRIPTION_CHAR_LIMIT],
265268
"project": {"key": context.jira.project},
266269
}
267-
268-
jira_response_create = self.client.create_issue(fields=fields)
270+
logger.debug(
271+
"Creating new Jira issue for Bug %s",
272+
bug.id,
273+
extra={"fields": fields, **context.model_dump()},
274+
)
275+
try:
276+
response = self.client.create_issue(fields=fields)
277+
except requests.HTTPError as exc:
278+
assert exc.response is not None
279+
try:
280+
response = exc.response.json()
281+
except json.JSONDecodeError:
282+
response = exc.response.text
283+
284+
logger.exception(
285+
"Failed to create issue for Bug %s",
286+
bug.id,
287+
extra={"response": response, **context.model_dump()},
288+
)
289+
raise JiraCreateError(f"Failed to create issue for Bug {bug.id}") from exc
269290

270291
# Jira response can be of the form: List or Dictionary
271-
if isinstance(jira_response_create, list):
272-
# if a list is returned, get the first item
273-
jira_response_create = jira_response_create[0]
274-
275-
if isinstance(jira_response_create, dict):
276-
# if a dict is returned or the first item in a list, confirm there are no errors
277-
errs = ",".join(jira_response_create.get("errors", []))
278-
msgs = ",".join(jira_response_create.get("errorMessages", []))
279-
if errs or msgs:
280-
raise JiraCreateError(errs + msgs)
281-
282-
return jira_response_create
292+
# if a list is returned, get the first item
293+
issue_data = response[0] if isinstance(response, list) else response
294+
logger.debug(
295+
"Jira issue %s created for Bug %s",
296+
issue_data["key"],
297+
bug.id,
298+
extra={"response": response, **context.model_dump()},
299+
)
300+
return issue_data
283301

284302
def add_jira_comment(self, context: ActionContext):
285303
"""Publish a comment on the specified Jira issue"""
@@ -455,14 +473,22 @@ def update_issue_resolution(self, context: ActionContext, jira_resolution: str):
455473
assert issue_key # Until we have more fine-grained typing of contexts
456474

457475
logger.debug(
458-
"Updating Jira resolution to %s",
476+
"Updating resolution of Jira issue %s to %s",
477+
issue_key,
459478
jira_resolution,
460479
extra=context.model_dump(),
461480
)
462-
return self.client.update_issue_field(
481+
response = self.client.update_issue_field(
463482
key=issue_key,
464483
fields={"resolution": jira_resolution},
465484
)
485+
logger.debug(
486+
"Updated resolution of Jira issue %s to %s",
487+
issue_key,
488+
jira_resolution,
489+
extra={"response": response, **context.model_dump()},
490+
)
491+
return response
466492

467493
def update_issue_components(
468494
self,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ warn_return_any = true
8383
plugins = "pydantic.mypy"
8484

8585
[[tool.mypy.overrides]]
86-
module = ["ruamel", "bugzilla", "atlassian", "statsd.defaults.env"]
86+
module = ["ruamel", "bugzilla", "atlassian", "atlassian.rest_client", "statsd.defaults.env"]
8787
ignore_missing_imports = true
8888

8989
[[tool.mypy.overrides]]

tests/unit/services/test_jira.py

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,14 @@ def test_jira_does_not_retry_4XX(mocked_responses, context_create_example):
9090
mocked_responses.add(
9191
responses.POST,
9292
url,
93-
json={},
93+
json={"errorMessages": ["You done goofed"]},
9494
status=400,
9595
)
9696

97-
with pytest.raises(requests.HTTPError):
97+
with pytest.raises(jira.JiraCreateError):
9898
jira.get_service().create_jira_issue(
9999
context=context_create_example, description="", issue_type="Task"
100100
)
101-
102101
assert len(mocked_responses.calls) == 1
103102

104103

@@ -155,3 +154,232 @@ def test_all_projects_components_exist_no_components_param(
155154
)
156155
result = jira.get_service()._all_projects_components_exist(actions)
157156
assert result is True
157+
158+
159+
def test_get_issue(mocked_responses, action_context_factory, capturelogs):
160+
context = action_context_factory()
161+
url = f"{get_settings().jira_base_url}rest/api/2/issue/JBI-234"
162+
mock_response_data = {"key": "JBI-234", "fields": {"project": {"key": "JBI"}}}
163+
mocked_responses.add(responses.GET, url, json=mock_response_data)
164+
165+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
166+
response = jira.get_service().get_issue(context=context, issue_key="JBI-234")
167+
168+
assert response == mock_response_data
169+
for record in capturelogs.records:
170+
assert record.rid == context.rid
171+
assert record.action["whiteboard_tag"] == context.action.whiteboard_tag
172+
173+
before, after = capturelogs.messages
174+
assert before == "Getting issue JBI-234"
175+
assert after == "Received issue JBI-234"
176+
177+
178+
def test_get_issue_handles_404(mocked_responses, action_context_factory, capturelogs):
179+
context = action_context_factory()
180+
url = f"{get_settings().jira_base_url}rest/api/2/issue/JBI-234"
181+
mocked_responses.add(responses.GET, url, status=404)
182+
183+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
184+
return_val = jira.get_service().get_issue(context=context, issue_key="JBI-234")
185+
186+
assert return_val is None
187+
188+
for record in capturelogs.records:
189+
assert record.rid == context.rid
190+
191+
before, after = capturelogs.records
192+
assert before.levelno == logging.DEBUG
193+
assert before.message == "Getting issue JBI-234"
194+
195+
assert after.levelno == logging.ERROR
196+
assert after.message.startswith("Could not read issue JBI-234")
197+
198+
199+
def test_get_issue_reraises_other_erroring_status_codes(
200+
mocked_responses, action_context_factory, capturelogs
201+
):
202+
context = action_context_factory()
203+
url = f"{get_settings().jira_base_url}rest/api/2/issue/JBI-234"
204+
mocked_responses.add(responses.GET, url, status=401)
205+
206+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
207+
with pytest.raises(requests.HTTPError):
208+
jira.get_service().get_issue(context=context, issue_key="JBI-234")
209+
210+
[record] = capturelogs.records
211+
assert record.rid == context.rid
212+
assert record.levelno == logging.DEBUG
213+
assert record.message == "Getting issue JBI-234"
214+
215+
216+
def test_update_issue_resolution(mocked_responses, action_context_factory, capturelogs):
217+
context = action_context_factory(jira__issue="JBI-234")
218+
url = f"{get_settings().jira_base_url}rest/api/2/issue/JBI-234"
219+
mocked_responses.add(
220+
responses.PUT,
221+
url,
222+
match=[
223+
responses.matchers.json_params_matcher({"fields": {"resolution": "DONEZO"}})
224+
],
225+
)
226+
227+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
228+
jira.get_service().update_issue_resolution(
229+
context=context, jira_resolution="DONEZO"
230+
)
231+
232+
for record in capturelogs.records:
233+
assert record.rid == context.rid
234+
assert record.levelno == logging.DEBUG
235+
236+
before, after = capturelogs.messages
237+
assert before == "Updating resolution of Jira issue JBI-234 to DONEZO"
238+
assert after == "Updated resolution of Jira issue JBI-234 to DONEZO"
239+
240+
241+
def test_update_issue_resolution_raises(
242+
mocked_responses, action_context_factory, capturelogs
243+
):
244+
context = action_context_factory(jira__issue="JBI-234")
245+
url = f"{get_settings().jira_base_url}rest/api/2/issue/JBI-234"
246+
mocked_responses.add(
247+
responses.PUT,
248+
url,
249+
status=401,
250+
match=[
251+
responses.matchers.json_params_matcher({"fields": {"resolution": "DONEZO"}})
252+
],
253+
)
254+
255+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
256+
with pytest.raises(requests.HTTPError):
257+
jira.get_service().update_issue_resolution(
258+
context=context, jira_resolution="DONEZO"
259+
)
260+
261+
[record] = capturelogs.records
262+
assert record.rid == context.rid
263+
assert record.levelno == logging.DEBUG
264+
assert record.message == "Updating resolution of Jira issue JBI-234 to DONEZO"
265+
266+
267+
def test_create_jira_issue(mocked_responses, action_context_factory, capturelogs):
268+
context = action_context_factory(jira__project_key="JBI")
269+
url = f"{get_settings().jira_base_url}rest/api/2/issue"
270+
mocked_response_data = {"key": "JBI-234"}
271+
issue_fields = {
272+
"summary": context.bug.summary,
273+
"issuetype": {"name": "Task"},
274+
"description": "description",
275+
"project": {"key": "JBI"},
276+
}
277+
mocked_responses.add(
278+
responses.POST,
279+
url,
280+
status=201,
281+
match=[
282+
responses.matchers.json_params_matcher(
283+
{"fields": issue_fields},
284+
)
285+
],
286+
json=mocked_response_data,
287+
)
288+
289+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
290+
response = jira.get_service().create_jira_issue(
291+
context=context, description="description", issue_type="Task"
292+
)
293+
294+
assert response == mocked_response_data
295+
296+
for record in capturelogs.records:
297+
assert record.rid == context.rid
298+
assert record.levelno == logging.DEBUG
299+
300+
before, after = capturelogs.records
301+
assert before.message == f"Creating new Jira issue for Bug {context.bug.id}"
302+
assert before.fields == issue_fields
303+
304+
assert after.message == f"Jira issue JBI-234 created for Bug {context.bug.id}"
305+
assert after.response == mocked_response_data
306+
307+
308+
def test_create_jira_issue_when_list_is_returned(
309+
mocked_responses, action_context_factory, capturelogs
310+
):
311+
context = action_context_factory(jira__project_key="JBI")
312+
url = f"{get_settings().jira_base_url}rest/api/2/issue"
313+
mocked_issue_data = {"key": "JBI-234"}
314+
issue_fields = {
315+
"summary": context.bug.summary,
316+
"issuetype": {"name": "Task"},
317+
"description": "description",
318+
"project": {"key": "JBI"},
319+
}
320+
mocked_responses.add(
321+
responses.POST,
322+
url,
323+
status=201,
324+
match=[
325+
responses.matchers.json_params_matcher(
326+
{"fields": issue_fields},
327+
)
328+
],
329+
json=[mocked_issue_data],
330+
)
331+
332+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
333+
issue_data = jira.get_service().create_jira_issue(
334+
context=context, description="description", issue_type="Task"
335+
)
336+
337+
assert issue_data == mocked_issue_data
338+
339+
before, after = capturelogs.records
340+
assert before.message == f"Creating new Jira issue for Bug {context.bug.id}"
341+
assert before.levelno == logging.DEBUG
342+
assert before.rid == context.rid
343+
assert before.fields == issue_fields
344+
345+
assert after.message == f"Jira issue JBI-234 created for Bug {context.bug.id}"
346+
assert after.levelno == logging.DEBUG
347+
assert after.rid == context.rid
348+
assert after.response == [mocked_issue_data]
349+
350+
351+
def test_create_jira_issue_returns_errors(
352+
mocked_responses, action_context_factory, capturelogs
353+
):
354+
context = action_context_factory(jira__project_key="JBI")
355+
url = f"{get_settings().jira_base_url}rest/api/2/issue"
356+
fake_error_data = {
357+
"errorMessages": ["You done goofed"],
358+
"errors": ["You messed up this time"],
359+
}
360+
issue_fields = {
361+
"summary": context.bug.summary,
362+
"issuetype": {"name": "Task"},
363+
"description": "description",
364+
"project": {"key": "JBI"},
365+
}
366+
mocked_responses.add(responses.POST, url, status=400, json=fake_error_data)
367+
368+
with capturelogs.for_logger("jbi.services.jira").at_level(logging.DEBUG):
369+
with pytest.raises(jira.JiraCreateError) as exc:
370+
jira.get_service().create_jira_issue(
371+
context=context, description="description", issue_type="Task"
372+
)
373+
374+
assert str(exc.value) == "Failed to create issue for Bug 654321"
375+
376+
before, after = capturelogs.records
377+
assert before.message == f"Creating new Jira issue for Bug {context.bug.id}"
378+
assert before.rid == context.rid
379+
assert before.levelno == logging.DEBUG
380+
assert before.fields == issue_fields
381+
382+
assert after.message == f"Failed to create issue for Bug {context.bug.id}"
383+
assert after.rid == context.rid
384+
assert after.levelno == logging.ERROR
385+
assert after.response == fake_error_data

tests/unit/test_steps.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,7 @@ def test_added_comment(
250250
def test_jira_returns_an_error(
251251
context_create_example: ActionContext, mocked_jira, action_params_factory
252252
):
253-
mocked_jira.create_issue.return_value = [
254-
{"errors": ["Boom"]},
255-
]
253+
mocked_jira.create_issue.side_effect = [JiraCreateError("Boom")]
256254
callable_object = Executor(
257255
action_params_factory(jira_project_key=context_create_example.jira.project)
258256
)

0 commit comments

Comments
 (0)