Skip to content

Commit 36a16d9

Browse files
committed
Replace asgi "extension" with RequestResponseTiming class
1 parent 960ab1c commit 36a16d9

File tree

4 files changed

+51
-20
lines changed

4 files changed

+51
-20
lines changed

uvicorn/logging.py

+3-3
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
@@ -148,8 +149,7 @@ def request_headers(self):
148149

149150
@property
150151
def duration(self):
151-
d = self.scope["response_end_time"] - self.scope["request_start_time"]
152-
return d
152+
return self.timing.total_duration_seconds()
153153

154154
def on_asgi_message(self, message):
155155
if message["type"] == "http.response.start":

uvicorn/protocols/http/h11_impl.py

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import asyncio
22
import http
33
import logging
4-
import time
54
import traceback
65
from typing import Callable
76
from urllib.parse import unquote
@@ -17,6 +16,7 @@
1716
service_unavailable,
1817
)
1918
from uvicorn.protocols.utils import (
19+
RequestResponseTiming,
2020
get_client_addr,
2121
get_local_addr,
2222
get_path_with_query_string,
@@ -174,11 +174,6 @@ def handle_events(self):
174174
"raw_path": raw_path,
175175
"query_string": query_string,
176176
"headers": self.headers,
177-
"extensions": {
178-
"uvicorn_request_duration": {
179-
"request_start_time": time.monotonic(),
180-
}
181-
},
182177
}
183178

184179
for name, value in self.headers:
@@ -212,6 +207,7 @@ def handle_events(self):
212207
message_event=asyncio.Event(),
213208
on_response=self.on_response_complete,
214209
)
210+
self.cycle.timing.request_started()
215211
task = self.loop.create_task(self.cycle.run_asgi(app))
216212
task.add_done_callback(self.tasks.discard)
217213
self.tasks.add(task)
@@ -229,6 +225,7 @@ def handle_events(self):
229225
self.transport.resume_reading()
230226
self.conn.start_next_cycle()
231227
continue
228+
self.cycle.timing.request_ended()
232229
self.cycle.more_body = False
233230
self.cycle.message_event.set()
234231

@@ -373,10 +370,11 @@ def __init__(
373370
# Response state
374371
self.response_started = False
375372
self.response_complete = False
373+
self.timing = RequestResponseTiming()
376374

377375
# For logging
378376
if self.gunicorn_log:
379-
self.gunicorn_atoms = GunicornSafeAtoms(self.scope)
377+
self.gunicorn_atoms = GunicornSafeAtoms(self.scope, self.timing)
380378
else:
381379
self.gunicorn_atoms = None
382380

@@ -443,6 +441,7 @@ async def send(self, message):
443441

444442
self.response_started = True
445443
self.waiting_for_100_continue = False
444+
self.timing.response_started()
446445

447446
status_code = message["status"]
448447
headers = self.default_headers + message.get("headers", [])
@@ -492,8 +491,7 @@ async def send(self, message):
492491
event = h11.EndOfMessage()
493492
output = self.conn.send(event)
494493

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

uvicorn/protocols/http/httptools_impl.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
service_unavailable,
1919
)
2020
from uvicorn.protocols.utils import (
21+
RequestResponseTiming,
2122
get_client_addr,
2223
get_local_addr,
2324
get_path_with_query_string,
@@ -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,4 +1,5 @@
11
import asyncio
2+
import time
23
import urllib.parse
34
from typing import Optional, Tuple
45

@@ -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)