Skip to content

Commit 52b781c

Browse files
authored
Improve logging and status reporting of actions (#155)
* Add operation field in log context of most actions logging * Replace action execution status with typed Operation * Document and add specific for actions return values * Rewrite Operations enum with Graham's pattern * Return True/False instead of operation * Log level has to be set explicitly in tests * Update action docs
1 parent e44f3b9 commit 52b781c

File tree

9 files changed

+196
-77
lines changed

9 files changed

+196
-77
lines changed

src/jbi/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Module domain specific code related to JBI.
3+
4+
This part of the code is not aware of the HTTP context it runs in.
5+
"""
6+
from enum import Enum
7+
from typing import Dict, Tuple
8+
9+
10+
class Operation(str, Enum):
11+
"""Enumeration of possible operations logged during WebHook execution."""
12+
13+
HANDLE = "handle"
14+
EXECUTE = "execute"
15+
IGNORE = "ignore"
16+
SUCCESS = "success"
17+
18+
CREATE = "create"
19+
UPDATE = "update"
20+
DELETE = "delete"
21+
COMMENT = "comment"
22+
LINK = "link"
23+
24+
25+
ActionResult = Tuple[bool, Dict]

src/jbi/runner.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,14 @@
66
from statsd.defaults.env import statsd
77

88
from src.app.environment import Settings
9+
from src.jbi import Operation
910
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
1011
from src.jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError
1112
from src.jbi.models import Actions
1213

1314
logger = logging.getLogger(__name__)
1415

1516

16-
class Operations:
17-
"""Track status of incoming requests in log entries."""
18-
19-
HANDLE = "handle"
20-
EXECUTE = "execute"
21-
IGNORE = "ignore"
22-
SUCCESS = "success"
23-
24-
2517
@statsd.timer("jbi.action.execution.timer")
2618
def execute_action(
2719
request: BugzillaWebhookRequest,
@@ -44,7 +36,7 @@ def execute_action(
4436
try:
4537
logger.debug(
4638
"Handling incoming request",
47-
extra={"operation": Operations.HANDLE, **log_context},
39+
extra={"operation": Operation.HANDLE, **log_context},
4840
)
4941
if not request.bug:
5042
raise IgnoreInvalidRequestError("no bug data received")
@@ -77,24 +69,27 @@ def execute_action(
7769
action.whiteboard_tag,
7870
action.module,
7971
bug_obj.id,
80-
extra={"operation": Operations.EXECUTE, **log_context},
72+
extra={"operation": Operation.EXECUTE, **log_context},
8173
)
8274

83-
content = action.caller(payload=request)
75+
handled, details = action.caller(payload=request)
8476

8577
logger.info(
8678
"Action %r executed successfully for Bug %s",
8779
action.whiteboard_tag,
8880
bug_obj.id,
89-
extra={"operation": Operations.SUCCESS, **log_context},
81+
extra={
82+
"operation": Operation.SUCCESS if handled else Operation.IGNORE,
83+
**log_context,
84+
},
9085
)
9186
statsd.incr("jbi.bugzilla.processed.count")
92-
return content
87+
return details
9388
except IgnoreInvalidRequestError as exception:
9489
logger.debug(
9590
"Ignore incoming request: %s",
9691
exception,
97-
extra={"operation": Operations.IGNORE, **log_context},
92+
extra={"operation": Operation.IGNORE, **log_context},
9893
)
9994
statsd.incr("jbi.bugzilla.ignored.count")
10095
raise

src/jbi/whiteboard_actions/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ Let's create a `new_action`!
77
1. First, add a new Python file (eg. `my_team_action.py`) in the `src/jbi/whiteboard_actions/` directory.
88
1. Add the Python function `init` to the module, for example:
99
```python
10+
from src.jbi import ActionResult, Operation
11+
1012
def init(jira_project_key, optional_param=42):
11-
return lambda payload: print(f"{optional_param}, going to {jira_project_key}!")
13+
14+
def execute(payload) -> ActionResult:
15+
print(f"{optional_param}, going to {jira_project_key}!")
16+
return True, {"result": 42}
17+
18+
return execute
1219
```
1320
1. In the above example the `jira_project_key` parameter is required
1421
1. `optional_param`, which has a default value, is not required to run this action
1522
1. `init()` returns a `__call__`able object that the system calls with the Bugzilla request payload
23+
1. The returned `ActionResult` features a boolean to indicate whether something was performed or not, along with a `Dict` (used as a response to the WebHook endpoint).
1624
1. Use the `payload` to perform the desired processing!
17-
1. Use the available service calls from `src/jbi/services.py' (or make new ones)
25+
1. Use the available service calls from `src/jbi/services.py` (or make new ones)
1826
1. Update the `README.md` to document your action
1927
1. Now the action `src.jbi.whiteboard_actions.my_team_actions` can be used in the YAML configuration, under the `module` key.

src/jbi/whiteboard_actions/default.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88

99
from src.app.environment import get_settings
10+
from src.jbi import ActionResult, Operation
1011
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
1112
from src.jbi.errors import ActionError
1213
from src.jbi.services import get_bugzilla, get_jira
@@ -33,7 +34,7 @@ def __init__(self, jira_project_key, **kwargs):
3334

3435
def __call__( # pylint: disable=inconsistent-return-statements
3536
self, payload: BugzillaWebhookRequest
36-
):
37+
) -> ActionResult:
3738
"""Called from BZ webhook when default action is used. All default-action webhook-events are processed here."""
3839
target = payload.event.target # type: ignore
3940
if target == "comment":
@@ -45,17 +46,20 @@ def __call__( # pylint: disable=inconsistent-return-statements
4546
target,
4647
extra={
4748
"request": payload.dict(),
49+
"operation": Operation.IGNORE,
4850
},
4951
)
52+
return False, {}
5053

51-
def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
54+
def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResult:
5255
"""Confirm issue is already linked, then apply comments; otherwise noop"""
5356
bug_obj = payload.bugzilla_object
5457
linked_issue_key = bug_obj.extract_from_see_also()
5558

5659
log_context = {
5760
"request": payload.dict(),
5861
"bug": bug_obj.dict(),
62+
"operation": Operation.COMMENT,
5963
"jira": {
6064
"issue": linked_issue_key,
6165
"project": self.jira_project_key,
@@ -67,11 +71,15 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
6771
bug_obj.id,
6872
extra=log_context,
6973
)
70-
return {"status": "noop"}
74+
return False, {}
7175

7276
comment = payload.map_as_jira_comment()
7377
if comment is None:
74-
return {"status": "noop"}
78+
logger.debug(
79+
"No matching comment found in payload",
80+
extra=log_context,
81+
)
82+
return False, {}
7583

7684
jira_response = self.jira_client.issue_add_comment(
7785
issue_key=linked_issue_key,
@@ -82,7 +90,7 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
8290
linked_issue_key,
8391
extra=log_context,
8492
)
85-
return {"status": "comment", "jira_response": jira_response}
93+
return True, {"jira_response": jira_response}
8694

8795
def jira_comments_for_update(
8896
self,
@@ -102,7 +110,7 @@ def update_issue(
102110

103111
def bug_create_or_update(
104112
self, payload: BugzillaWebhookRequest
105-
): # pylint: disable=too-many-locals
113+
) -> ActionResult: # pylint: disable=too-many-locals
106114
"""Create and link jira issue with bug, or update; rollback if multiple events fire"""
107115
bug_obj = payload.bugzilla_object
108116
linked_issue_key = bug_obj.extract_from_see_also() # type: ignore
@@ -121,7 +129,10 @@ def bug_create_or_update(
121129
"Update fields of Jira issue %s for Bug %s",
122130
linked_issue_key,
123131
bug_obj.id,
124-
extra=log_context,
132+
extra={
133+
**log_context,
134+
"operation": Operation.LINK,
135+
},
125136
)
126137
jira_response_update = self.jira_client.update_issue_field(
127138
key=linked_issue_key, fields=bug_obj.map_as_jira_issue()
@@ -134,7 +145,10 @@ def bug_create_or_update(
134145
"Create comment #%s on Jira issue %s",
135146
i + 1,
136147
linked_issue_key,
137-
extra=log_context,
148+
extra={
149+
**log_context,
150+
"operation": Operation.COMMENT,
151+
},
138152
)
139153
jira_response_comments.append(
140154
self.jira_client.issue_add_comment(
@@ -144,14 +158,11 @@ def bug_create_or_update(
144158

145159
self.update_issue(payload, bug_obj, linked_issue_key, is_new=False)
146160

147-
return {
148-
"status": "update",
149-
"jira_responses": [jira_response_update, jira_response_comments],
150-
}
161+
return True, {"jira_responses": [jira_response_update, jira_response_comments]}
151162

152-
def create_and_link_issue(
163+
def create_and_link_issue( # pylint: disable=too-many-locals
153164
self, payload, bug_obj
154-
): # pylint: disable=too-many-locals
165+
) -> ActionResult:
155166
"""create jira issue and establish link between bug and issue; rollback/delete if required"""
156167
log_context = {
157168
"request": payload.dict(),
@@ -163,7 +174,10 @@ def create_and_link_issue(
163174
logger.debug(
164175
"Create new Jira issue for Bug %s",
165176
bug_obj.id,
166-
extra=log_context,
177+
extra={
178+
**log_context,
179+
"operation": Operation.CREATE,
180+
},
167181
)
168182
comment_list = self.bugzilla_client.get_comments(idlist=[bug_obj.id])
169183
fields = {
@@ -205,19 +219,25 @@ def create_and_link_issue(
205219
"Delete duplicated Jira issue %s from Bug %s",
206220
jira_key_in_response,
207221
bug_obj.id,
208-
extra=log_context,
222+
extra={
223+
**log_context,
224+
"operation": Operation.DELETE,
225+
},
209226
)
210227
jira_response_delete = self.jira_client.delete_issue(
211228
issue_id_or_key=jira_key_in_response
212229
)
213-
return {"status": "duplicate", "jira_response": jira_response_delete}
230+
return True, {"jira_response": jira_response_delete}
214231

215232
jira_url = f"{settings.jira_base_url}browse/{jira_key_in_response}"
216233
logger.debug(
217234
"Link %r on Bug %s",
218235
jira_url,
219236
bug_obj.id,
220-
extra=log_context,
237+
extra={
238+
**log_context,
239+
"operation": Operation.LINK,
240+
},
221241
)
222242
update = self.bugzilla_client.build_update(see_also_add=jira_url)
223243
bugzilla_response = self.bugzilla_client.update_bugs([bug_obj.id], update)
@@ -227,7 +247,10 @@ def create_and_link_issue(
227247
"Link %r on Jira issue %s",
228248
bugzilla_url,
229249
jira_key_in_response,
230-
extra=log_context,
250+
extra={
251+
**log_context,
252+
"operation": Operation.LINK,
253+
},
231254
)
232255
jira_response = self.jira_client.create_or_update_issue_remote_links(
233256
issue_key=jira_key_in_response,
@@ -237,8 +260,7 @@ def create_and_link_issue(
237260

238261
self.update_issue(payload, bug_obj, jira_key_in_response, is_new=True)
239262

240-
return {
241-
"status": "create",
263+
return True, {
242264
"bugzilla_response": bugzilla_response,
243265
"jira_response": jira_response,
244266
}

src/jbi/whiteboard_actions/default_with_assignee_and_status.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010
import logging
1111

12+
from src.jbi import Operation
1213
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
1314
from src.jbi.whiteboard_actions.default import DefaultExecutor
1415

@@ -58,6 +59,7 @@ def update_issue(
5859
"project": self.jira_project_key,
5960
},
6061
"changed_fields": changed_fields,
62+
"operation": Operation.UPDATE,
6163
}
6264

6365
def clear_assignee():
@@ -99,7 +101,10 @@ def clear_assignee():
99101
# assignee.
100102
clear_assignee()
101103
else:
102-
logger.debug("No assignee found", extra=log_context)
104+
logger.debug(
105+
"No assignee found",
106+
extra={**log_context, "operation": Operation.IGNORE},
107+
)
103108
clear_assignee()
104109

105110
# If this is a new issue or if the bug's status or resolution has
@@ -120,5 +125,9 @@ def clear_assignee():
120125
else:
121126
logger.debug(
122127
"Bug status was not in the status map.",
123-
extra={**log_context, "status_map": self.status_map},
128+
extra={
129+
**log_context,
130+
"status_map": self.status_map,
131+
"operation": Operation.IGNORE,
132+
},
124133
)

tests/unit/jbi/noop_action.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
55
`init` should return a __call__able
66
"""
7+
from src.jbi import Operation
78

89

910
def init(**parameters):
10-
return lambda payload: {"parameters": parameters, "payload": payload.json()}
11+
return lambda payload: (
12+
True,
13+
{"parameters": parameters, "payload": payload.json()},
14+
)

0 commit comments

Comments
 (0)