Skip to content

Commit c543258

Browse files
authored
Improve logging using context (fixes #73) (#74)
1 parent 8589053 commit c543258

File tree

8 files changed

+125
-56
lines changed

8 files changed

+125
-56
lines changed

src/app/log.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ def configure_logging():
3030
},
3131
"loggers": {
3232
"request.summary": {"handlers": ["console"], "level": "INFO"},
33-
"src.jbi": {"handlers": ["console"], "level": "INFO"},
34-
"ignored-requests": {"handlers": ["console"], "level": "DEBUG"},
33+
"src.jbi": {"handlers": ["console"], "level": "DEBUG"},
3534
},
3635
}
3736

src/jbi/bugzilla.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import datetime
77
import json
88
import logging
9-
import traceback
109
from typing import Dict, List, Optional
1110
from urllib.parse import ParseResult, urlparse
1211

1312
from pydantic import BaseModel # pylint: disable=no-name-in-module
1413

15-
bugzilla_logger = logging.getLogger("src.jbi.bugzilla")
14+
logger = logging.getLogger(__name__)
15+
16+
JIRA_HOSTNAMES = ("jira", "atlassian")
1617

1718

1819
class BugzillaWebhookUser(BaseModel):
@@ -166,16 +167,25 @@ def extract_from_see_also(self):
166167
for url in self.see_also: # pylint: disable=not-an-iterable
167168
try:
168169
parsed_url: ParseResult = urlparse(url=url)
169-
expected_hosts = ["jira", "atlassian"]
170+
host_parts = parsed_url.hostname.split(".")
171+
except (ValueError, AttributeError):
172+
logger.debug(
173+
"Bug %s `see_also` is not a URL: %s",
174+
self.id,
175+
url,
176+
extra={
177+
"bug": {
178+
"id": self.id,
179+
}
180+
},
181+
)
182+
continue
170183

171-
if any( # pylint: disable=use-a-generator
172-
[part in expected_hosts for part in parsed_url.hostname.split(".")]
173-
):
174-
parsed_jira_key = parsed_url.path.split("/")[-1]
184+
if any(part in JIRA_HOSTNAMES for part in host_parts):
185+
parsed_jira_key = parsed_url.path.rstrip("/").split("/")[-1]
186+
if parsed_jira_key: # URL ending with /
175187
return parsed_jira_key
176-
except Exception: # pylint: disable=broad-except
177-
# Try parsing all see_also fields; log errors.
178-
bugzilla_logger.debug(traceback.format_exc())
188+
179189
return None
180190

181191

src/jbi/configuration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from src.jbi.models import Actions
1111

1212
settings = environment.get_settings()
13-
jbi_logger = logging.getLogger("src.jbi")
13+
logger = logging.getLogger(__name__)
1414

1515

1616
class ConfigError(Exception):
@@ -29,7 +29,7 @@ def get_actions(
2929
actions: Actions = Actions.parse_raw(yaml_data)
3030
return actions
3131
except ValidationError as exception:
32-
jbi_logger.exception(exception)
32+
logger.exception(exception)
3333
raise ConfigError("Errors exist.") from exception
3434

3535

src/jbi/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def validate_action_matches_whiteboard(
5858
if not actions:
5959
raise ValueError("no actions configured")
6060
for name, action in actions.items():
61-
if name != action.parameters["whiteboard_tag"]:
61+
if name.lower() != action.parameters["whiteboard_tag"]:
6262
raise ValueError("action name must match whiteboard tag")
6363

6464
return actions

src/jbi/router.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import importlib
55
import logging
66
from types import ModuleType
7-
from typing import Dict, List, Optional
7+
from typing import Dict, List, Mapping, Optional, Tuple
88

99
from fastapi import APIRouter, Body, Depends, Request
1010
from fastapi.responses import HTMLResponse, JSONResponse
@@ -20,53 +20,93 @@
2020

2121
api_router = APIRouter(tags=["JBI"])
2222

23-
jbi_logger = logging.getLogger("src.jbi.router")
24-
invalid_logger = logging.getLogger("ignored-requests")
23+
logger = logging.getLogger(__name__)
2524

2625

27-
def extract_current_action( # pylint: disable=inconsistent-return-statements
28-
bug_obj: BugzillaBug, action_map
29-
):
26+
class Operations:
27+
"""Track status of incoming requests in log entries."""
28+
29+
HANDLE = "handle"
30+
EXECUTE = "execute"
31+
IGNORE = "ignore"
32+
SUCCESS = "success"
33+
34+
35+
def extract_current_action(
36+
bug_obj: BugzillaBug, action_map: Mapping[str, Dict]
37+
) -> Optional[Tuple[str, Dict]]:
3038
"""Find first matching action from bug's whiteboard list"""
3139
potential_configuration_tags: List[
3240
str
3341
] = bug_obj.get_potential_whiteboard_config_list()
3442

3543
for tag in potential_configuration_tags:
3644
if action := action_map.get(tag.lower()):
37-
return action
45+
return tag.lower(), action
46+
return None
3847

3948

40-
def execute_action(request: BugzillaWebhookRequest, action_map, settings):
49+
def execute_action(
50+
request: BugzillaWebhookRequest,
51+
action_map: Mapping[str, Dict],
52+
settings: environment.Settings,
53+
):
4154
"""Execute action"""
55+
log_context = {
56+
"bug": {
57+
"id": request.bug.id if request.bug else None,
58+
},
59+
"request": request.json(),
60+
}
4261
try:
43-
jbi_logger.info("request: %s", request.json())
62+
logger.debug(
63+
"Handling incoming request",
64+
extra={"operation": Operations.HANDLE, **log_context},
65+
)
4466
if not request.bug:
4567
raise IgnoreInvalidRequestError("no bug data received")
4668

4769
bug_obj = getbug_as_bugzilla_object(request=request)
48-
current_action = extract_current_action(bug_obj, action_map) # type: ignore
70+
log_context["bug"] = bug_obj.json()
4971

50-
if not current_action:
72+
action_item = extract_current_action(bug_obj, action_map)
73+
if not action_item:
5174
raise IgnoreInvalidRequestError(
5275
"whiteboard tag not found in configured actions"
5376
)
5477

78+
action_name, current_action = action_item
79+
log_context["action"] = current_action
80+
5581
if bug_obj.is_private and not current_action["allow_private"]:
5682
raise IgnoreInvalidRequestError(
57-
"private bugs are not valid for this action"
83+
f"private bugs are not valid for action {action_name!r}"
5884
)
5985

60-
jbi_logger.info("\nrequest: %s, \naction: %s", request.json(), current_action)
86+
logger.info(
87+
"Execute action %r for Bug %s",
88+
action_name,
89+
bug_obj.id,
90+
extra={"operation": Operations.EXECUTE, **log_context},
91+
)
6192
action_module: ModuleType = importlib.import_module(current_action["action"])
6293
callable_action = action_module.init( # type: ignore
6394
**current_action["parameters"]
6495
)
6596
content = callable_action(payload=request)
66-
jbi_logger.info("request: %s, content: %s", request.json(), content)
97+
logger.info(
98+
"Action %r executed successfully for Bug %s",
99+
action_name,
100+
bug_obj.id,
101+
extra={"operation": Operations.SUCCESS, **log_context},
102+
)
67103
return JSONResponse(content=content, status_code=200)
68104
except IgnoreInvalidRequestError as exception:
69-
invalid_logger.debug("ignore-invalid-request: %s", exception)
105+
logger.debug(
106+
"Ignore incoming request: %s",
107+
exception,
108+
extra={"operation": Operations.IGNORE, **log_context},
109+
)
70110
return JSONResponse(content={"error": str(exception)}, status_code=200)
71111

72112

src/jbi/services.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Services and functions that can be used to create custom actions"""
2-
import logging
32
from typing import Dict, List
43

54
import bugzilla as rh_bugzilla # type: ignore
@@ -9,7 +8,6 @@
98
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
109

1110
settings = environment.get_settings()
12-
services_logger = logging.getLogger("src.jbi.services")
1311

1412

1513
def get_jira():

tests/unit/jbi/test_bugzilla.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,25 @@ def test_get_jira_labels_multiple():
4242
"[test-both.space-and-not]",
4343
]
4444
assert expected == jira_labels
45+
46+
47+
@pytest.mark.parametrize(
48+
"see_also,expected",
49+
[
50+
([], None),
51+
(["foo"], None),
52+
(["fail:/format"], None),
53+
(["foo", "http://jira.net/123"], "123"),
54+
(["http://bugzilla.org/123"], None),
55+
(["http://jira.com"], None),
56+
(["http://mozilla.jira.com/"], None),
57+
(["http://mozilla.jira.com/123"], "123"),
58+
(["http://mozilla.jira.com/123/"], "123"),
59+
(["http://mozilla.jira.com/ticket/123"], "123"),
60+
(["http://atlassian.com/ticket/123"], "123"),
61+
(["http://mozilla.jira.com/123", "http://mozilla.jira.com/456"], "123"),
62+
],
63+
)
64+
def test_extract_see_also(see_also, expected):
65+
test_bug = bugzilla.BugzillaBug(id=0, see_also=see_also)
66+
assert test_bug.extract_from_see_also() == expected

tests/unit/jbi/test_router.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_request_is_ignored_because_private(
2121
test_action = Action.parse_obj({"action": "tests.unit.jbi.noop_action"}).dict()
2222

2323
with mock.patch("src.jbi.router.extract_current_action") as mocked_extract_action:
24-
mocked_extract_action.return_value = test_action
24+
mocked_extract_action.return_value = "test", test_action
2525
with mock.patch("src.jbi.router.getbug_as_bugzilla_object") as mocked_bz_func:
2626
mocked_bz_func.return_value = private_webhook_request_example.bug
2727
with TestClient(app) as anon_client:
@@ -34,17 +34,17 @@ def test_request_is_ignored_because_private(
3434
assert response.status_code == 200
3535
assert (
3636
response.json()["error"]
37-
== "private bugs are not valid for this action"
37+
== "private bugs are not valid for action 'test'"
3838
)
3939

40-
invalid_request_logs = caplog.records[1]
41-
assert invalid_request_logs.name == "ignored-requests"
40+
captured_log_msgs = [
41+
r.msg % r.args for r in caplog.records if r.name == "src.jbi.router"
42+
]
4243

43-
assert invalid_request_logs.msg == "ignore-invalid-request: %s"
44-
assert invalid_request_logs.args
45-
for arg in invalid_request_logs.args:
46-
assert isinstance(arg, IgnoreInvalidRequestError)
47-
assert str(arg) == "private bugs are not valid for this action"
44+
assert captured_log_msgs == [
45+
"Handling incoming request",
46+
"Ignore incoming request: private bugs are not valid for action 'test'",
47+
]
4848

4949

5050
def test_private_request_is_allowed(
@@ -57,7 +57,7 @@ def test_private_request_is_allowed(
5757
).dict()
5858

5959
with mock.patch("src.jbi.router.extract_current_action") as mocked_extract_action:
60-
mocked_extract_action.return_value = test_action
60+
mocked_extract_action.return_value = "test", test_action
6161
with mock.patch("src.jbi.router.getbug_as_bugzilla_object") as mocked_bz_func:
6262
mocked_bz_func.return_value = private_webhook_request_example.bug
6363
with TestClient(app) as anon_client:
@@ -88,14 +88,14 @@ def test_request_is_ignored_because_no_bug(
8888
assert response.status_code == 200
8989
assert response.json()["error"] == "no bug data received"
9090

91-
invalid_request_logs = caplog.records[1]
92-
assert invalid_request_logs.name == "ignored-requests"
91+
captured_log_msgs = [
92+
r.msg % r.args for r in caplog.records if r.name == "src.jbi.router"
93+
]
9394

94-
assert invalid_request_logs.msg == "ignore-invalid-request: %s"
95-
assert invalid_request_logs.args
96-
for arg in invalid_request_logs.args:
97-
assert isinstance(arg, IgnoreInvalidRequestError)
98-
assert str(arg) == "no bug data received"
95+
assert captured_log_msgs == [
96+
"Handling incoming request",
97+
"Ignore incoming request: no bug data received",
98+
]
9999

100100

101101
def test_request_is_ignored_because_no_action(
@@ -120,11 +120,11 @@ def test_request_is_ignored_because_no_action(
120120
== "whiteboard tag not found in configured actions"
121121
)
122122

123-
invalid_request_logs = caplog.records[1]
124-
assert invalid_request_logs.name == "ignored-requests"
123+
captured_log_msgs = [
124+
r.msg % r.args for r in caplog.records if r.name == "src.jbi.router"
125+
]
125126

126-
assert invalid_request_logs.msg == "ignore-invalid-request: %s"
127-
assert invalid_request_logs.args
128-
for arg in invalid_request_logs.args:
129-
assert isinstance(arg, IgnoreInvalidRequestError)
130-
assert str(arg) == "whiteboard tag not found in configured actions"
127+
assert captured_log_msgs == [
128+
"Handling incoming request",
129+
"Ignore incoming request: whiteboard tag not found in configured actions",
130+
]

0 commit comments

Comments
 (0)