Skip to content

Commit f29f09e

Browse files
committed
Replace asgi "extension" with RequestResponseTiming class
1 parent 8598899 commit f29f09e

File tree

4 files changed

+55
-21
lines changed

4 files changed

+55
-21
lines changed

uvicorn/logging.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ class GunicornSafeAtoms(abc.Mapping):
130130
- escapes double quotes found in atom strings
131131
"""
132132

133-
def __init__(self, scope):
133+
def __init__(self, scope, timing):
134134
self.scope = scope
135+
self.timing = timing
135136
self.status_code = None
136137
self.response_headers = {}
137138
self.response_length = 0
@@ -142,20 +143,21 @@ def __init__(self, scope):
142143
def request_headers(self):
143144
if self._request_headers is None:
144145
self._request_headers = {
145-
k.decode("ascii"): v.decode("ascii") for k, v in self.scope["headers"]
146+
k.decode("ascii"): v.decode("ascii")
147+
for k, v in self.scope["headers"]
146148
}
147149
return self._request_headers
148150

149151
@property
150152
def duration(self):
151-
d = self.scope["response_end_time"] - self.scope["request_start_time"]
152-
return d
153+
return self.timing.total_duration_seconds()
153154

154155
def on_asgi_message(self, message):
155156
if message["type"] == "http.response.start":
156157
self.status_code = message["status"]
157158
self.response_headers = {
158-
k.decode("ascii"): v.decode("ascii") for k, v in message["headers"]
159+
k.decode("ascii"): v.decode("ascii")
160+
for k, v in message["headers"]
159161
}
160162
elif message["type"] == "http.response.body":
161163
self.response_length += len(message.get("body", ""))

uvicorn/protocols/http/h11_impl.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
get_path_with_query_string,
2323
get_remote_addr,
2424
is_ssl,
25+
RequestResponseTiming,
2526
)
2627

2728

@@ -174,11 +175,6 @@ def handle_events(self):
174175
"raw_path": raw_path,
175176
"query_string": query_string,
176177
"headers": self.headers,
177-
"extensions": {
178-
"uvicorn_request_duration": {
179-
"request_start_time": time.monotonic(),
180-
}
181-
},
182178
}
183179

184180
for name, value in self.headers:
@@ -212,6 +208,7 @@ def handle_events(self):
212208
message_event=asyncio.Event(),
213209
on_response=self.on_response_complete,
214210
)
211+
self.cycle.timing.request_started()
215212
task = self.loop.create_task(self.cycle.run_asgi(app))
216213
task.add_done_callback(self.tasks.discard)
217214
self.tasks.add(task)
@@ -229,6 +226,7 @@ def handle_events(self):
229226
self.transport.resume_reading()
230227
self.conn.start_next_cycle()
231228
continue
229+
self.cycle.timing.request_ended()
232230
self.cycle.more_body = False
233231
self.cycle.message_event.set()
234232

@@ -373,10 +371,11 @@ def __init__(
373371
# Response state
374372
self.response_started = False
375373
self.response_complete = False
374+
self.timing = RequestResponseTiming()
376375

377376
# For logging
378377
if self.gunicorn_log:
379-
self.gunicorn_atoms = GunicornSafeAtoms(self.scope)
378+
self.gunicorn_atoms = GunicornSafeAtoms(self.scope, self.timing)
380379
else:
381380
self.gunicorn_atoms = None
382381

@@ -443,6 +442,7 @@ async def send(self, message):
443442

444443
self.response_started = True
445444
self.waiting_for_100_continue = False
445+
self.timing.response_started()
446446

447447
status_code = message["status"]
448448
headers = self.default_headers + message.get("headers", [])
@@ -492,8 +492,7 @@ async def send(self, message):
492492
event = h11.EndOfMessage()
493493
output = self.conn.send(event)
494494

495-
duration_scope = self.scope["extensions"]["uvicorn_request_duration"]
496-
duration_scope["response_end_time"] = time.monotonic()
495+
self.timing.response_ended()
497496
if self.gunicorn_log is not None:
498497
try:
499498
self.gunicorn_log.access_log.info(

uvicorn/protocols/http/httptools_impl.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
get_path_with_query_string,
2424
get_remote_addr,
2525
is_ssl,
26+
RequestResponseTiming,
2627
)
2728

2829
HEADER_RE = re.compile(b'[\x00-\x1F\x7F()<>@,;:[]={} \t\\"]')
@@ -86,6 +87,7 @@ def __init__(
8687
self.headers = None
8788
self.expect_100_continue = False
8889
self.cycle = None
90+
self.request_start_time = None
8991

9092
# Protocol interface
9193
def connection_made(self, transport):
@@ -211,13 +213,11 @@ def on_url(self, url):
211213
"raw_path": raw_path,
212214
"query_string": parsed_url.query if parsed_url.query else b"",
213215
"headers": self.headers,
214-
"extensions": {
215-
"uvicorn_request_duration": {
216-
"request_start_time": time.monotonic(),
217-
}
218-
},
219216
}
220217

218+
def on_message_begin(self):
219+
self.request_start_time = time.monotonic()
220+
221221
def on_header(self, name: bytes, value: bytes):
222222
name = name.lower()
223223
if name == b"expect" and value.lower() == b"100-continue":
@@ -257,6 +257,7 @@ def on_headers_complete(self):
257257
keep_alive=http_version != "1.0",
258258
on_response=self.on_response_complete,
259259
)
260+
self.cycle.timing.request_start_time = self.request_start_time
260261
if existing_cycle is None or existing_cycle.response_complete:
261262
# Standard case - start processing the request.
262263
task = self.loop.create_task(self.cycle.run_asgi(app))
@@ -280,6 +281,7 @@ def on_message_complete(self):
280281
return
281282
self.cycle.more_body = False
282283
self.cycle.message_event.set()
284+
self.cycle.timing.request_ended()
283285

284286
def on_response_complete(self):
285287
# Callback for pipelined HTTP requests to be started.
@@ -376,10 +378,11 @@ def __init__(
376378
self.response_complete = False
377379
self.chunked_encoding = None
378380
self.expected_content_length = 0
381+
self.timing = RequestResponseTiming()
379382

380383
# For logging.
381384
if self.gunicorn_log:
382-
self.gunicorn_atoms = GunicornSafeAtoms(self.scope)
385+
self.gunicorn_atoms = GunicornSafeAtoms(self.scope, self.timing)
383386
else:
384387
self.gunicorn_atoms = None
385388

@@ -445,6 +448,7 @@ async def send(self, message):
445448
raise RuntimeError(msg % message_type)
446449

447450
self.response_started = True
451+
self.timing.response_started()
448452
self.waiting_for_100_continue = False
449453

450454
status_code = message["status"]
@@ -528,8 +532,7 @@ async def send(self, message):
528532
if self.expected_content_length != 0:
529533
raise RuntimeError("Response content shorter than Content-Length")
530534
self.response_complete = True
531-
duration_scope = self.scope["extensions"]["uvicorn_request_duration"]
532-
duration_scope["response_end_time"] = time.monotonic()
535+
self.timing.response_ended()
533536

534537
if self.gunicorn_log is not None:
535538
try:

uvicorn/protocols/utils.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import urllib.parse
3+
import time
34
from typing import Optional, Tuple
45

56
from asgiref.typing import WWWScope
@@ -54,3 +55,32 @@ def get_path_with_query_string(scope: WWWScope) -> str:
5455
path_with_query_string, scope["query_string"].decode("ascii")
5556
)
5657
return path_with_query_string
58+
59+
60+
class RequestResponseTiming:
61+
def __init__(self):
62+
self.request_start_time: Optional[int] = None
63+
self.request_end_time: Optional[int] = None
64+
self.response_start_time: Optional[int] = None
65+
self.response_end_time: Optional[int] = None
66+
67+
def request_started(self):
68+
self.request_start_time = time.monotonic()
69+
70+
def request_ended(self):
71+
self.request_end_time = time.monotonic()
72+
73+
def response_started(self):
74+
self.response_start_time = time.monotonic()
75+
76+
def response_ended(self):
77+
self.response_end_time = time.monotonic()
78+
79+
def request_duration_seconds(self):
80+
return self.request_end_time - self.request_start_time
81+
82+
def response_duration_seconds(self):
83+
return self.response_end_time - self.response_start_time
84+
85+
def total_duration_seconds(self):
86+
return self.response_end_time - self.request_start_time

0 commit comments

Comments
 (0)