Skip to content

Commit 36a5626

Browse files
committed
test
1 parent 29dee60 commit 36a5626

5 files changed

Lines changed: 99 additions & 91 deletions

File tree

manage_breast_screening/config/settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ def list_env(key):
284284
"correlation_id": {
285285
"()": "manage_breast_screening.core.middleware.exception_logging.CorrelationIdFilter",
286286
},
287+
"suppress_handled_500s": {
288+
"()": "manage_breast_screening.core.middleware.exception_logging.SuppressHandled500s",
289+
},
287290
},
288291
"formatters": {
289292
"verbose": {
@@ -312,8 +315,9 @@ def list_env(key):
312315
},
313316
"django.request": {
314317
"handlers": ["console"],
315-
"level": "CRITICAL", # suppresses 4xx/5xx; middleware handles exceptions
318+
"level": "INFO",
316319
"propagate": False,
320+
"filters": ["suppress_handled_500s"],
317321
},
318322
"django.db.backends": {
319323
"level": "DEBUG" if LOG_QUERIES else "INFO",

manage_breast_screening/core/middleware/exception_logging.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from contextvars import ContextVar
55

66
from django.utils.deprecation import MiddlewareMixin
7+
from opentelemetry import trace as otel_trace
78

89
from manage_breast_screening.core.services.application_insights_logging import (
910
ApplicationInsightsLogging,
@@ -21,26 +22,41 @@ def process_exception(self, request, exception):
2122
if user and hasattr(user, "is_authenticated") and user.is_authenticated
2223
else "Anonymous"
2324
)
24-
context = (
25-
f"URL: {request.path}, "
26-
f"Method: {request.method}, "
27-
f"User: {user_repr}, "
28-
f"Exception: {exception}"
29-
)
25+
3026
correlation_id = getattr(request, "correlation_id", None)
3127

3228
extra = {
3329
"url": request.path,
3430
"method": request.method,
3531
"user": user_repr,
36-
"exception_type": type(exception).__name__,
3732
"correlation_id": correlation_id,
33+
"exception_type": type(exception).__name__,
3834
}
39-
app_insights_logger.exception(context, extra=extra)
35+
extra_str = ", ".join(f"{k}={v}" for k, v in extra.items())
36+
app_insights_logger.exception(f"{extra_str} {exception}", extra=extra)
37+
38+
request._exception_logged = True
4039

4140
return None
4241

4342

43+
class SuppressHandled500s:
44+
"""
45+
Django's `django.request` logger logs every unhandled view exception at `ERROR` level
46+
automatically. `ExceptionLoggingMiddleware.process_exception` then logs the same exception
47+
again. Both reach Application Insights, creating duplicates in the `exceptions` / `traces`
48+
tables and inflating alert counts.
49+
"""
50+
51+
def filter(self, record):
52+
request = getattr(record, "request", None)
53+
54+
if request and getattr(request, "_exception_logged", False):
55+
return False
56+
57+
return True
58+
59+
4460
class CorrelationIdMiddleware(MiddlewareMixin):
4561
RESPONSE_HEADER = "X-Correlation-ID"
4662
CORRELATION_ID_HEADER = "HTTP_" + RESPONSE_HEADER.upper().replace("-", "_")
@@ -49,15 +65,26 @@ class CorrelationIdMiddleware(MiddlewareMixin):
4965
SAFE_ID_REGEX = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
5066

5167
def process_request(self, request):
68+
correlation_id = self.find_correlation_id(request)
69+
request.correlation_id = correlation_id
70+
request._correlation_id_token = correlation_id_ctx.set(correlation_id)
71+
72+
def find_correlation_id(self, request):
73+
"""
74+
Use correlation ID from incoming request headers if exists and is safe, otherwise use OTel trace ID if available, otherwise generate a new UUID.
75+
"""
76+
5277
raw_id = request.META.get(self.CORRELATION_ID_HEADER)
5378
if raw_id and self.SAFE_ID_REGEX.match(raw_id):
54-
correlation_id = raw_id
55-
else:
56-
correlation_id = str(uuid.uuid4())
57-
request.correlation_id = correlation_id
79+
return raw_id
5880

59-
# Store the token so we can reset later
60-
request._correlation_id_token = correlation_id_ctx.set(correlation_id)
81+
span = otel_trace.get_current_span()
82+
ctx = span.get_span_context()
83+
if ctx.is_valid:
84+
# Use OTel trace ID — this links to all other App Insights telemetry
85+
return format(ctx.trace_id, "032x")
86+
87+
return str(uuid.uuid4())
6188

6289
def process_response(self, request, response):
6390
correlation_id = getattr(request, "correlation_id", None)
@@ -82,5 +109,4 @@ def process_exception(self, request, exception):
82109
class CorrelationIdFilter(logging.Filter):
83110
def filter(self, record):
84111
record.correlation_id = correlation_id_ctx.get() or "-"
85-
86112
return True

manage_breast_screening/core/services/application_insights_logging.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def __init__(self) -> None:
1111
self.logger_name = os.getenv(
1212
"APPLICATIONINSIGHTS_LOGGER_NAME", "manage-breast-screening"
1313
)
14+
os.environ.setdefault("OTEL_SERVICE_NAME", self.logger_name)
1415
self.logger = self.getLogger()
1516

1617
def configure_azure_monitor(self):
@@ -31,18 +32,27 @@ def getLogger(self):
3132
return logging.getLogger(self.logger_name)
3233

3334
def exception(self, exception_name: str, extra: dict = None):
35+
# Keys in extra are forwarded to Application Insights
36+
# as customDimensions and become filterable in the Logs blade and alerting rules.
37+
# stack_info=True is redundant with logger.exception()
3438
self.logger.exception(exception_name, stack_info=False, extra=extra)
3539

40+
# can remove once notifications app is removed
3641
def custom_event_warning(self, message: str, event_name: str):
37-
extra = {
38-
"microsoft.custom_event.name": event_name,
39-
"additional_attrs": message,
40-
}
41-
self.logger.warning(message, extra=extra)
42+
self.logger.warning(
43+
message,
44+
extra={
45+
"microsoft.custom_event.name": event_name,
46+
"additional_attrs": message,
47+
},
48+
)
4249

50+
# can remove once notifications app is removed
4351
def custom_event_info(self, message: str, event_name: str):
44-
extra = {
45-
"microsoft.custom_event.name": event_name,
46-
"additional_attrs": message,
47-
}
48-
self.logger.info(message, extra=extra)
52+
self.logger.info(
53+
message,
54+
extra={
55+
"microsoft.custom_event.name": event_name,
56+
"additional_attrs": message,
57+
},
58+
)

manage_breast_screening/core/tests/middleware/test_exception_logging.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,15 @@ def test_process_exception_logs_with_authenticated_user(
5454

5555
middleware.process_exception(request, exception)
5656

57-
expected_context = (
58-
"URL: /test/, Method: GET, User: testuser, Exception: Something went wrong"
59-
)
6057
mock_logger.exception.assert_called_once_with(
61-
expected_context, correlation_id="corr-id-123"
58+
"url=/test/, method=GET, user=testuser, correlation_id=corr-id-123, exception_type=Exception Something went wrong",
59+
extra={
60+
"url": "/test/",
61+
"method": "GET",
62+
"user": "testuser",
63+
"correlation_id": "corr-id-123",
64+
"exception_type": "Exception",
65+
},
6266
)
6367

6468
def test_process_exception_logs_with_anonymous_user(self, monkeypatch, middleware):
@@ -74,11 +78,15 @@ def test_process_exception_logs_with_anonymous_user(self, monkeypatch, middlewar
7478

7579
middleware.process_exception(request, exception)
7680

77-
expected_context = (
78-
"URL: /test/, Method: GET, User: Anonymous, Exception: Details of exception"
79-
)
8081
mock_logger.exception.assert_called_once_with(
81-
expected_context, correlation_id=None
82+
"url=/test/, method=GET, user=Anonymous, correlation_id=None, exception_type=Exception Details of exception",
83+
extra={
84+
"url": "/test/",
85+
"method": "GET",
86+
"user": "Anonymous",
87+
"correlation_id": None,
88+
"exception_type": "Exception",
89+
},
8290
)
8391

8492
def test_process_exception_logs_with_no_user(self, monkeypatch, middleware):
@@ -93,9 +101,15 @@ def test_process_exception_logs_with_no_user(self, monkeypatch, middleware):
93101

94102
middleware.process_exception(request, exception)
95103

96-
expected_context = "URL: /test/, Method: GET, User: Anonymous, Exception: Fail"
97104
mock_logger.exception.assert_called_once_with(
98-
expected_context, correlation_id="abc"
105+
"url=/test/, method=GET, user=Anonymous, correlation_id=abc, exception_type=Exception Fail",
106+
extra={
107+
"url": "/test/",
108+
"method": "GET",
109+
"user": "Anonymous",
110+
"correlation_id": "abc",
111+
"exception_type": "Exception",
112+
},
99113
)
100114

101115
def test_process_exception_returns_none(self, middleware):
@@ -142,11 +156,15 @@ def test_process_exception_logs_correct_path_and_method(
142156

143157
middleware.process_exception(request, exception)
144158

145-
expected_context = (
146-
f"URL: {path}, Method: {method}, User: testuser, Exception: err"
147-
)
148159
mock_logger.exception.assert_called_once_with(
149-
expected_context, correlation_id="x"
160+
f"url={path}, method={method}, user=testuser, correlation_id=x, exception_type=Exception err",
161+
extra={
162+
"url": path,
163+
"method": method,
164+
"user": "testuser",
165+
"correlation_id": "x",
166+
"exception_type": "Exception",
167+
},
150168
)
151169

152170

@@ -298,15 +316,6 @@ def test_correlation_id_filter_uses_correlation_id_from_context(self, filter):
298316
assert filter.filter(record)
299317
assert record.correlation_id == test_correlation_id
300318

301-
def test_correlation_id_filter_preserves_existing_correlation_id(self, filter):
302-
correlation_id_ctx.set("context-correlation-id-123")
303-
304-
record = self._make_record()
305-
record.correlation_id = "record-correlation-id-123"
306-
307-
assert filter.filter(record)
308-
assert record.correlation_id == "record-correlation-id-123"
309-
310319
def test_correlation_id_filter_uses_default_when_not_set(self, filter):
311320
correlation_id_ctx.set(None)
312321

manage_breast_screening/core/tests/services/test_application_insights_logging.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,7 @@ def test_raise_exception(self, mock_logging, mock_configure_azure):
5858
ApplicationInsightsLogging().exception("CustomError")
5959
mock_logging.getLogger.assert_called_with("insights-logger")
6060
mock_logging.getLogger.return_value.exception.assert_called_with(
61-
"CustomError", stack_info=True, extra={}
62-
)
63-
64-
def test_raise_exception_with_correlation_id(
65-
self, mock_logging, mock_configure_azure
66-
):
67-
ApplicationInsightsLogging().exception("CustomError", correlation_id="12345")
68-
mock_logging.getLogger.assert_called_with("insights-logger")
69-
mock_logging.getLogger.return_value.exception.assert_called_with(
70-
"CustomError", stack_info=True, extra={"correlation_id": "12345"}
61+
"CustomError", stack_info=False, extra=None
7162
)
7263

7364
def test_custom_event_warning(self, mock_logging, mock_configure_azure):
@@ -83,22 +74,6 @@ def test_custom_event_warning(self, mock_logging, mock_configure_azure):
8374
},
8475
)
8576

86-
def test_custom_event_warning_with_correlation_id(
87-
self, mock_logging, mock_configure_azure
88-
):
89-
ApplicationInsightsLogging().custom_event_warning(
90-
"something went wrong", "custom-event", correlation_id="12345"
91-
)
92-
mock_logging.getLogger.assert_called_with("insights-logger")
93-
mock_logging.getLogger.return_value.warning.assert_called_with(
94-
"something went wrong",
95-
extra={
96-
"microsoft.custom_event.name": "custom-event",
97-
"additional_attrs": "something went wrong",
98-
"correlation_id": "12345",
99-
},
100-
)
101-
10277
def test_custom_event_info(self, mock_logging, mock_configure_azure):
10378
ApplicationInsightsLogging().custom_event_info(
10479
"some information", "custom-event"
@@ -111,19 +86,3 @@ def test_custom_event_info(self, mock_logging, mock_configure_azure):
11186
"additional_attrs": "some information",
11287
},
11388
)
114-
115-
def test_custom_event_info_with_correlation_id(
116-
self, mock_logging, mock_configure_azure
117-
):
118-
ApplicationInsightsLogging().custom_event_info(
119-
"some information", "custom-event", correlation_id="12345"
120-
)
121-
mock_logging.getLogger.assert_called_with("insights-logger")
122-
mock_logging.getLogger.return_value.info.assert_called_with(
123-
"some information",
124-
extra={
125-
"microsoft.custom_event.name": "custom-event",
126-
"additional_attrs": "some information",
127-
"correlation_id": "12345",
128-
},
129-
)

0 commit comments

Comments
 (0)