Skip to content

Commit c2562b2

Browse files
committed
Additional logging
1 parent d5cfbcd commit c2562b2

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

app/notifications/process_notifications.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
import uuid
23
from datetime import datetime
34

@@ -34,6 +35,7 @@
3435
dao_delete_notifications_by_id,
3536
)
3637
from app.models import Notification
38+
from app.request_timings import add_context, record_timing
3739
from app.utils import (
3840
parse_and_format_phone_number,
3941
try_download_template_email_file_from_s3,
@@ -189,9 +191,13 @@ def persist_notification(
189191

190192
# if simulated create a Notification model to return but do not persist the Notification to the dB
191193
if not simulated:
194+
db_insert_start = time.perf_counter()
192195
dao_create_notification(notification=notification, _autocommit=_autocommit)
196+
record_timing("db_insert_ms", time.perf_counter() - db_insert_start)
193197
# Not sure how we can rollback
198+
redis_limits_start = time.perf_counter()
194199
increment_daily_limit_caches(service, notification, key_type)
200+
record_timing("redis_daily_limit_ms", time.perf_counter() - redis_limits_start)
195201

196202
return notification
197203

@@ -236,7 +242,10 @@ def send_notification_to_queue_detached(key_type, notification_type, notificatio
236242
deliver_task = get_pdf_for_templated_letter
237243

238244
try:
245+
add_context(queue=queue, celery_task_name=getattr(deliver_task, "name", deliver_task.__name__))
246+
queue_publish_start = time.perf_counter()
239247
deliver_task.apply_async([str(notification_id)], queue=queue)
248+
record_timing("queue_publish_ms", time.perf_counter() - queue_publish_start)
240249
except Exception:
241250
dao_delete_notifications_by_id(notification_id)
242251
raise

app/request_timings.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from flask import g, has_request_context
4+
5+
6+
def init_request_timings() -> None:
7+
if not has_request_context():
8+
return
9+
if not hasattr(g, "request_timings"):
10+
g.request_timings = {}
11+
if not hasattr(g, "request_timing_context"):
12+
g.request_timing_context = {}
13+
14+
15+
def record_timing(name: str, duration_seconds: float) -> None:
16+
if not has_request_context():
17+
return
18+
init_request_timings()
19+
g.request_timings[name] = round(duration_seconds * 1000.0, 3)
20+
21+
22+
def add_context(**kwargs: object) -> None:
23+
if not has_request_context():
24+
return
25+
init_request_timings()
26+
g.request_timing_context.update({k: v for k, v in kwargs.items() if v is not None})
27+
28+
29+
def get_timings() -> tuple[dict[str, float], dict[str, object]]:
30+
if not has_request_context():
31+
return {}, {}
32+
timings = getattr(g, "request_timings", {})
33+
context = getattr(g, "request_timing_context", {})
34+
return timings, context

app/v2/notifications/post_notifications.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import functools
3+
import time
34
import uuid
45
from datetime import datetime
56

@@ -52,6 +53,7 @@
5253
validate_and_format_recipient,
5354
validate_template,
5455
)
56+
from app.request_timings import add_context, get_timings, init_request_timings, record_timing
5557
from app.schema_validation import validate
5658
from app.utils import try_parse_and_format_phone_number
5759
from app.v2.errors import BadRequestError
@@ -76,8 +78,37 @@
7678
)
7779

7880

81+
def _log_slow_request(request_start: float, notification_type: str, notification_id: str | None = None) -> None:
82+
request_time = time.perf_counter() - request_start
83+
if request_time <= 1.0:
84+
return
85+
86+
timings, context = get_timings()
87+
service_id = getattr(authenticated_service, "id", None)
88+
api_key_type = getattr(api_user, "key_type", None)
89+
add_context(
90+
notification_type=notification_type,
91+
service_id=str(service_id) if service_id is not None else None,
92+
api_key_type=api_key_type,
93+
endpoint=request.endpoint,
94+
path=request.path,
95+
notification_id=notification_id,
96+
request_time_seconds=round(request_time, 3),
97+
)
98+
_, context = get_timings()
99+
current_app.logger.info(
100+
"slow request breakdown",
101+
extra={
102+
"timings_ms": timings,
103+
**context,
104+
},
105+
)
106+
107+
79108
@v2_notification_blueprint.route(f"/{LETTER_TYPE}", methods=["POST"])
80109
def post_precompiled_letter_notification():
110+
init_request_timings()
111+
request_start = time.perf_counter()
81112
check_rate_limiting(authenticated_service, api_user, notification_type=LETTER_TYPE)
82113

83114
request_json = get_valid_json()
@@ -104,13 +135,21 @@ def post_precompiled_letter_notification():
104135
precompiled=True,
105136
)
106137

138+
notification_id = None
139+
if isinstance(notification, dict):
140+
notification_id = str(notification.get("id"))
141+
_log_slow_request(request_start, LETTER_TYPE, notification_id=notification_id)
142+
107143
return jsonify(notification), 201
108144

109145

110146
@v2_notification_blueprint.route("/<notification_type>", methods=["POST"])
111147
def post_notification(notification_type):
148+
init_request_timings()
149+
request_start = time.perf_counter()
112150
check_rate_limiting(authenticated_service, api_user, notification_type=notification_type)
113151

152+
json_parse_start = time.perf_counter()
114153
with POST_NOTIFICATION_JSON_PARSE_DURATION_SECONDS.time():
115154
request_json = get_valid_json()
116155

@@ -122,9 +161,11 @@ def post_notification(notification_type):
122161
form = validate(request_json, post_letter_request)
123162
else:
124163
abort(404)
164+
record_timing("json_parse_validate_ms", time.perf_counter() - json_parse_start)
125165

126166
check_service_has_permission(authenticated_service, notification_type)
127167

168+
validate_template_start = time.perf_counter()
128169
template, template_with_content = validate_template(
129170
form["template_id"],
130171
form.get("personalisation", {}),
@@ -133,6 +174,7 @@ def post_notification(notification_type):
133174
check_char_count=False,
134175
recipient=form.get("email_address"),
135176
)
177+
record_timing("validate_template_ms", time.perf_counter() - validate_template_start)
136178

137179
reply_to = get_reply_to_text(notification_type, form, template)
138180

@@ -156,6 +198,11 @@ def post_notification(notification_type):
156198
unsubscribe_link=form.get("one_click_unsubscribe_url", None),
157199
)
158200

201+
notification_id = None
202+
if isinstance(notification, dict):
203+
notification_id = str(notification.get("id"))
204+
_log_slow_request(request_start, notification_type, notification_id=notification_id)
205+
159206
return jsonify(notification), 201
160207

161208

@@ -172,27 +219,34 @@ def process_sms_or_email_notification(
172219
notification_id = uuid.uuid4()
173220
form_send_to = form["email_address"] if notification_type == EMAIL_TYPE else form["phone_number"]
174221

222+
recipient_validate_start = time.perf_counter()
175223
recipient_data = validate_and_format_recipient(
176224
send_to=form_send_to, key_type=api_user.key_type, service=service, notification_type=notification_type
177225
)
226+
record_timing("recipient_validate_ms", time.perf_counter() - recipient_validate_start)
178227

179228
send_to = recipient_data["normalised_to"] if type(recipient_data) is dict else recipient_data
180229

181230
# Do not persist or send notification to the queue if it is a simulated recipient
182231
simulated = simulated_recipient(send_to, notification_type)
183232

233+
document_upload_start = time.perf_counter()
184234
personalisation, document_download_count = process_document_uploads(
185235
form.get("personalisation"),
186236
service,
187237
send_to=send_to,
188238
simulated=simulated,
189239
)
240+
record_timing("document_upload_ms", time.perf_counter() - document_upload_start)
241+
add_context(document_download_count=document_download_count)
190242
if document_download_count:
191243
# We changed personalisation which means we need to update the content
192244
template_with_content.values = personalisation
193245

194246
# validate content length after url is replaced in personalisation.
247+
content_validation_start = time.perf_counter()
195248
check_is_message_too_long(template_with_content)
249+
record_timing("content_validation_ms", time.perf_counter() - content_validation_start)
196250

197251
response = create_response_for_post_notification(
198252
notification_id=notification_id,
@@ -206,6 +260,7 @@ def process_sms_or_email_notification(
206260
template_with_content=template_with_content,
207261
)
208262

263+
persist_start = time.perf_counter()
209264
persist_notification(
210265
notification_id=notification_id,
211266
template_id=template.id,
@@ -222,6 +277,7 @@ def process_sms_or_email_notification(
222277
unsubscribe_link=unsubscribe_link,
223278
document_download_count=document_download_count,
224279
)
280+
record_timing("persist_notification_ms", time.perf_counter() - persist_start)
225281

226282
if not simulated:
227283
send_notification_to_queue_detached(

0 commit comments

Comments
 (0)