2
2
import http
3
3
import logging
4
4
import re
5
+ import time
5
6
import urllib
7
+ from collections import abc
8
+ from os import getpid
6
9
from typing import Callable
7
10
8
11
import httptools
@@ -39,6 +42,174 @@ def _get_status_line(status_code):
39
42
}
40
43
41
44
45
+ class GunicornSafeAtoms (abc .Mapping ):
46
+ def __init__ (self , scope ):
47
+ self .scope = scope
48
+ self .status_code = None
49
+ self .response_headers = {}
50
+ self .response_length = 0
51
+
52
+ self ._request_headers = None
53
+
54
+ @property
55
+ def request_headers (self ):
56
+ if self ._request_headers is None :
57
+ self ._request_headers = dict (self .scope ['headers' ])
58
+ return self ._request_headers
59
+
60
+ @property
61
+ def duration (self ):
62
+ duration_extension = self .scope ['extensions' ]['uvicorn_request_duration' ]
63
+ d = duration_extension ['response_end_time' ] - duration_extension ['request_start_time' ]
64
+ return d
65
+
66
+ def on_asgi_message (self , message ):
67
+ if message ['type' ] == 'http.response.start' :
68
+ self .status_code = message ['status' ]
69
+ self .response_headers = dict (message ['headers' ])
70
+ elif message ['type' ] == 'http.response.body' :
71
+ self .response_length += len (message .get ('body' , '' ))
72
+
73
+ def _request_header (self , key ):
74
+ return self .request_headers .get (key .lower ())
75
+
76
+ def _response_header (self , key ):
77
+ return self .response_headers .get (key .lower ())
78
+
79
+ def _wsgi_environ_variable (self , key ):
80
+ # FIXME: provide fallbacks to access WSGI environ (at least the
81
+ # required variables).
82
+ return None
83
+
84
+ def __getitem__ (self , key ):
85
+ if key in self .HANDLERS :
86
+ retval = self .HANDLERS [key ](self )
87
+ elif key .startswith ('{' ):
88
+ if key .endswith ('}i' ):
89
+ retval = self ._request_header (key [1 :- 2 ])
90
+ elif key .endswith ('}o' ):
91
+ retval = self ._response_header (key [1 :- 2 ])
92
+ elif key .endswith ('}e' ):
93
+ retval = self ._wsgi_environ_variable (key [1 :- 2 ])
94
+ else :
95
+ retval = None
96
+ else :
97
+ retval = None
98
+
99
+ if retval is None :
100
+ return '-'
101
+ if isinstance (retval , str ):
102
+ return retval .replace ('"' , '\\ "' )
103
+ return retval
104
+
105
+ HANDLERS = {}
106
+
107
+ def _register_handler (key , handlers = HANDLERS ):
108
+ def decorator (fn ):
109
+ handlers [key ] = fn
110
+ return fn
111
+ return decorator
112
+
113
+ @_register_handler ('h' )
114
+ def _remote_address (self , * args , ** kwargs ):
115
+ return self .scope ['client' ][0 ]
116
+
117
+ @_register_handler ('l' )
118
+ def _dash (self , * args , ** kwargs ):
119
+ return '-'
120
+
121
+ @_register_handler ('u' )
122
+ def _user_name (self , * args , ** kwargs ):
123
+ pass
124
+
125
+ @_register_handler ('t' )
126
+ def date_of_the_request (self , * args , ** kwargs ):
127
+ """Date and time in Apache Common Log Format"""
128
+ return time .strftime ('[%d/%b/%Y:%H:%M:%S %z]' )
129
+
130
+ @_register_handler ('r' )
131
+ def status_line (self , * args , ** kwargs ):
132
+ full_raw_path = (self .scope ['raw_path' ] + self .scope ['query_string' ])
133
+ full_path = full_raw_path .decode ('ascii' )
134
+ return '{method} {full_path} HTTP/{http_version}' .format (
135
+ full_path = full_path , ** self .scope
136
+ )
137
+
138
+ @_register_handler ('m' )
139
+ def request_method (self , * args , ** kwargs ):
140
+ return self .scope ['method' ]
141
+
142
+ @_register_handler ('U' )
143
+ def url_path (self , * args , ** kwargs ):
144
+ return self .scope ['raw_path' ].decode ('ascii' )
145
+
146
+ @_register_handler ('q' )
147
+ def query_string (self , * args , ** kwargs ):
148
+ return self .scope ['query_string' ].decode ('ascii' )
149
+
150
+ @_register_handler ('H' )
151
+ def protocol (self , * args , ** kwargs ):
152
+ return 'HTTP/%s' % self .scope ['http_version' ]
153
+
154
+ @_register_handler ('s' )
155
+ def status (self , * args , ** kwargs ):
156
+ return self .status_code or '-'
157
+
158
+ @_register_handler ('B' )
159
+ def response_length (self , * args , ** kwargs ):
160
+ return self .response_length
161
+
162
+ @_register_handler ('b' )
163
+ def response_length_or_dash (self , * args , ** kwargs ):
164
+ return self .response_length or '-'
165
+
166
+ @_register_handler ('f' )
167
+ def referer (self , * args , ** kwargs ):
168
+ val = self .request_headers .get (b'referer' )
169
+ if val is None :
170
+ return None
171
+ return val .decode ('ascii' )
172
+
173
+ @_register_handler ('a' )
174
+ def user_agent (self , * args , ** kwargs ):
175
+ val = self .request_headers .get (b'user-agent' )
176
+ if val is None :
177
+ return None
178
+ return val .decode ('ascii' )
179
+
180
+ @_register_handler ('T' )
181
+ def request_time_seconds (self , * args , ** kwargs ):
182
+ return int (self .duration )
183
+
184
+ @_register_handler ('D' )
185
+ def request_time_microseconds (self , * args , ** kwargs ):
186
+ return int (self .duration * 1_000_000 )
187
+
188
+ @_register_handler ('L' )
189
+ def request_time_decimal_seconds (self , * args , ** kwargs ):
190
+ return "%.6f" % self .duration
191
+
192
+ @_register_handler ('p' )
193
+ def process_id (self , * args , ** kwargs ):
194
+ return "<%s>" % getpid ()
195
+
196
+ def __iter__ (self ):
197
+ # FIXME: add WSGI environ
198
+ yield from self .HANDLERS
199
+ for k , _ in self .scope ['headers' ]:
200
+ yield '{%s}i' % k .lower ()
201
+ for k in self .response_headers :
202
+ yield '{%s}o' % k .lower ()
203
+
204
+ def __len__ (self ):
205
+ # FIXME: add WSGI environ
206
+ return (
207
+ len (self .HANDLERS )
208
+ + len (self .scope ['headers' ] or ())
209
+ + len (self .response_headers )
210
+ )
211
+
212
+
42
213
class HttpToolsProtocol (asyncio .Protocol ):
43
214
def __init__ (
44
215
self , config , server_state , on_connection_lost : Callable = None , _loop = None
@@ -53,6 +224,7 @@ def __init__(
53
224
self .logger = logging .getLogger ("uvicorn.error" )
54
225
self .access_logger = logging .getLogger ("uvicorn.access" )
55
226
self .access_log = self .access_logger .hasHandlers ()
227
+ self .gunicorn_log = config .gunicorn_log
56
228
self .parser = httptools .HttpRequestParser (self )
57
229
self .ws_protocol_class = config .ws_protocol_class
58
230
self .root_path = config .root_path
@@ -211,6 +383,11 @@ def on_url(self, url):
211
383
"raw_path" : raw_path ,
212
384
"query_string" : parsed_url .query if parsed_url .query else b"" ,
213
385
"headers" : self .headers ,
386
+ "extensions" : {
387
+ "uvicorn_request_duration" : {
388
+ "request_start_time" : time .monotonic (),
389
+ }
390
+ },
214
391
}
215
392
216
393
def on_header (self , name : bytes , value : bytes ):
@@ -245,6 +422,7 @@ def on_headers_complete(self):
245
422
logger = self .logger ,
246
423
access_logger = self .access_logger ,
247
424
access_log = self .access_log ,
425
+ gunicorn_log = self .gunicorn_log ,
248
426
default_headers = self .default_headers ,
249
427
message_event = asyncio .Event (),
250
428
expect_100_continue = self .expect_100_continue ,
@@ -338,6 +516,7 @@ def __init__(
338
516
logger ,
339
517
access_logger ,
340
518
access_log ,
519
+ gunicorn_log ,
341
520
default_headers ,
342
521
message_event ,
343
522
expect_100_continue ,
@@ -350,6 +529,7 @@ def __init__(
350
529
self .logger = logger
351
530
self .access_logger = access_logger
352
531
self .access_log = access_log
532
+ self .gunicorn_log = gunicorn_log
353
533
self .default_headers = default_headers
354
534
self .message_event = message_event
355
535
self .on_response = on_response
@@ -369,6 +549,12 @@ def __init__(
369
549
self .chunked_encoding = None
370
550
self .expected_content_length = 0
371
551
552
+ # For logging.
553
+ if self .gunicorn_log :
554
+ self .gunicorn_atoms = GunicornSafeAtoms (self .scope )
555
+ else :
556
+ self .gunicorn_atoms = None
557
+
372
558
# ASGI exception wrapper
373
559
async def run_asgi (self , app ):
374
560
try :
@@ -415,6 +601,9 @@ async def send_500_response(self):
415
601
async def send (self , message ):
416
602
message_type = message ["type" ]
417
603
604
+ if self .gunicorn_atoms is not None :
605
+ self .gunicorn_atoms .on_asgi_message (message )
606
+
418
607
if self .flow .write_paused and not self .disconnected :
419
608
await self .flow .drain ()
420
609
@@ -436,7 +625,7 @@ async def send(self, message):
436
625
if CLOSE_HEADER in self .scope ["headers" ] and CLOSE_HEADER not in headers :
437
626
headers = headers + [CLOSE_HEADER ]
438
627
439
- if self .access_log :
628
+ if self .access_log and not self . gunicorn_log :
440
629
self .access_logger .info (
441
630
'%s - "%s %s HTTP/%s" %d' ,
442
631
get_client_addr (self .scope ),
@@ -511,6 +700,19 @@ async def send(self, message):
511
700
if self .expected_content_length != 0 :
512
701
raise RuntimeError ("Response content shorter than Content-Length" )
513
702
self .response_complete = True
703
+ duration_extension = self .scope ['extensions' ]['uvicorn_request_duration' ]
704
+ duration_extension ['response_end_time' ] = time .monotonic ()
705
+
706
+ if self .gunicorn_log :
707
+ try :
708
+ self .gunicorn_log .access_log .info (
709
+ self .gunicorn_log .cfg .access_log_format ,
710
+ self .gunicorn_atoms ,
711
+ )
712
+ except :
713
+ import traceback
714
+ self .gunicorn_log .error (traceback .format_exc ())
715
+
514
716
self .message_event .set ()
515
717
if not self .keep_alive :
516
718
self .transport .close ()
0 commit comments