Skip to content

Commit 44edff8

Browse files
authored
Access logger improvements (#2145)
* Fix #2123: User request instead of message in access logger * Doc update * Optimize request.host property * Introduce BaseRequest.remote property * Fix spelling errors * Add tests
1 parent bb646d1 commit 44edff8

11 files changed

+274
-205
lines changed

aiohttp/hdrs.py

+1
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,6 @@
9191
WANT_DIGEST = istr('WANT-DIGEST')
9292
WARNING = istr('WARNING')
9393
WWW_AUTHENTICATE = istr('WWW-AUTHENTICATE')
94+
X_FORWARDED_FOR = istr('X-FORWARDED-FOR')
9495
X_FORWARDED_HOST = istr('X-FORWARDED-HOST')
9596
X_FORWARDED_PROTO = istr('X-FORWARDED-PROTO')

aiohttp/helpers.py

+31-44
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ class AccessLogger:
306306
log = logging.getLogger("spam")
307307
log_format = "%a %{User-Agent}i"
308308
access_logger = AccessLogger(log, log_format)
309-
access_logger.log(message, environ, response, transport, time)
309+
access_logger.log(request, response, time)
310310
311311
Format:
312312
%% The percent sign
@@ -337,7 +337,6 @@ class AccessLogger:
337337
'D': 'request_time_micro',
338338
'i': 'request_header',
339339
'o': 'response_header',
340-
'e': 'environ'
341340
}
342341

343342
LOG_FORMAT = '%a %l %u %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i"'
@@ -408,76 +407,65 @@ def compile_format(self, log_format):
408407
return log_format, methods
409408

410409
@staticmethod
411-
def _format_e(key, args):
412-
return (args[1] or {}).get(key, '-')
413-
414-
@staticmethod
415-
def _format_i(key, args):
416-
if not args[0]:
410+
def _format_i(key, request, response, time):
411+
if request is None:
417412
return '(no headers)'
418413

419414
# suboptimal, make istr(key) once
420-
return args[0].headers.get(key, '-')
415+
return request.headers.get(key, '-')
421416

422417
@staticmethod
423-
def _format_o(key, args):
418+
def _format_o(key, request, response, time):
424419
# suboptimal, make istr(key) once
425-
return args[2].headers.get(key, '-')
420+
return response.headers.get(key, '-')
426421

427422
@staticmethod
428-
def _format_a(args):
429-
if args[3] is None:
423+
def _format_a(request, response, time):
424+
if request is None:
430425
return '-'
431-
peername = args[3].get_extra_info('peername')
432-
if isinstance(peername, (list, tuple)):
433-
return peername[0]
434-
else:
435-
return peername
426+
ip = request.remote
427+
return ip if ip is not None else '-'
436428

437429
@staticmethod
438-
def _format_t(args):
430+
def _format_t(request, response, time):
439431
return datetime.datetime.utcnow().strftime('[%d/%b/%Y:%H:%M:%S +0000]')
440432

441433
@staticmethod
442-
def _format_P(args):
434+
def _format_P(request, response, time):
443435
return "<%s>" % os.getpid()
444436

445437
@staticmethod
446-
def _format_r(args):
447-
msg = args[0]
448-
if not msg:
438+
def _format_r(request, response, time):
439+
if request is None:
449440
return '-'
450-
return '%s %s HTTP/%s.%s' % tuple((msg.method,
451-
msg.path) + msg.version)
452-
453-
@staticmethod
454-
def _format_s(args):
455-
return args[2].status
441+
return '%s %s HTTP/%s.%s' % tuple((request.method,
442+
request.path_qs) + request.version)
456443

457444
@staticmethod
458-
def _format_b(args):
459-
return args[2].body_length
445+
def _format_s(request, response, time):
446+
return response.status
460447

461448
@staticmethod
462-
def _format_O(args):
463-
return args[2].body_length
449+
def _format_b(request, response, time):
450+
return response.body_length
464451

465452
@staticmethod
466-
def _format_T(args):
467-
return round(args[4])
453+
def _format_T(request, response, time):
454+
return round(time)
468455

469456
@staticmethod
470-
def _format_Tf(args):
471-
return '%06f' % args[4]
457+
def _format_Tf(request, response, time):
458+
return '%06f' % time
472459

473460
@staticmethod
474-
def _format_D(args):
475-
return round(args[4] * 1000000)
461+
def _format_D(request, response, time):
462+
return round(time * 1000000)
476463

477-
def _format_line(self, args):
478-
return ((key, method(args)) for key, method in self._methods)
464+
def _format_line(self, request, response, time):
465+
return ((key, method(request, response, time))
466+
for key, method in self._methods)
479467

480-
def log(self, message, environ, response, transport, time):
468+
def log(self, request, response, time):
481469
"""Log access.
482470
483471
:param message: Request object. May be None.
@@ -487,8 +475,7 @@ def log(self, message, environ, response, transport, time):
487475
:param float time: Time taken to serve the request.
488476
"""
489477
try:
490-
fmt_info = self._format_line(
491-
[message, environ, response, transport, time])
478+
fmt_info = self._format_line(request, response, time)
492479

493480
values = list()
494481
extra = dict()

aiohttp/web_protocol.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,9 @@ def force_close(self, send_last_heartbeat=False):
349349
self.transport.close()
350350
self.transport = None
351351

352-
def log_access(self, message, environ, response, time):
353-
if self.access_logger:
354-
self.access_logger.log(message, environ, response,
355-
self.transport, time)
352+
def log_access(self, request, response, time):
353+
if self.access_logger is not None:
354+
self.access_logger.log(request, response, time)
356355

357356
def log_debug(self, *args, **kw):
358357
if self.debug:
@@ -444,7 +443,7 @@ def start(self, message, payload, handler):
444443

445444
# log access
446445
if self.access_log:
447-
self.log_access(message, None, resp, loop.time() - now)
446+
self.log_access(request, resp, loop.time() - now)
448447

449448
# check payload
450449
if not payload.is_eof():

aiohttp/web_request.py

+31-5
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def version(self):
275275

276276
@reify
277277
def host(self):
278-
""" Hostname of the request.
278+
"""Hostname of the request.
279279
280280
Hostname is resolved through the following headers, in this order:
281281
@@ -288,12 +288,38 @@ def host(self):
288288
host = next(
289289
(f['host'] for f in self.forwarded if 'host' in f), None
290290
)
291-
if not host and hdrs.X_FORWARDED_HOST in self._message.headers:
292-
host = self._message.headers[hdrs.X_FORWARDED_HOST]
293-
elif hdrs.HOST in self._message.headers:
294-
host = self._message.headers[hdrs.HOST]
291+
if host is None:
292+
host = self._message.headers.get(hdrs.X_FORWARDED_HOST)
293+
if host is None:
294+
host = self._message.headers.get(hdrs.HOST)
295295
return host
296296

297+
@reify
298+
def remote(self):
299+
"""Remote IP of client initiated HTTP request.
300+
301+
The IP is resolved through the following headers, in this order:
302+
303+
- Forwarded
304+
- X-Forwarded-For
305+
- peername of opened socket
306+
"""
307+
ip = next(
308+
(f['for'] for f in self.forwarded if 'for' in f), None
309+
)
310+
if ip is None:
311+
ips = self._message.headers.get(hdrs.X_FORWARDED_FOR)
312+
if ips is not None:
313+
ip = ips.split(',')[0].strip()
314+
if ip is None:
315+
transport = self._transport
316+
peername = transport.get_extra_info('peername')
317+
if isinstance(peername, (list, tuple)):
318+
ip = peername[0]
319+
else:
320+
ip = peername
321+
return ip
322+
297323
@reify
298324
def url(self):
299325
return URL('{}://{}{}'.format(self._scheme,

changes/2123.feature

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Use request for getting access log information instead of message/transport pair.
2+
3+
Add `RequestBase.remote` property for accessing to IP of client
4+
initiated HTTP request.

changes/2123.removal

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Drop %O format from logging, use %b instead.
2+
3+
Drop %e format from logging, environment variables are not supported anymore.
4+

docs/logging.rst

-4
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ request and response:
7171
+--------------+---------------------------------------------------------+
7272
| ``%b`` | Size of response in bytes, excluding HTTP headers |
7373
+--------------+---------------------------------------------------------+
74-
| ``%O`` | Bytes sent, including headers |
75-
+--------------+---------------------------------------------------------+
7674
| ``%T`` | The time taken to serve the request, in seconds |
7775
+--------------+---------------------------------------------------------+
7876
| ``%Tf`` | The time taken to serve the request, in seconds |
@@ -84,8 +82,6 @@ request and response:
8482
+--------------+---------------------------------------------------------+
8583
| ``%{FOO}o`` | ``response.headers['FOO']`` |
8684
+--------------+---------------------------------------------------------+
87-
| ``%{FOO}e`` | ``os.environ['FOO']`` |
88-
+--------------+---------------------------------------------------------+
8985

9086
Default access log format is::
9187

docs/spelling_wordlist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Paolini
156156
param
157157
params
158158
pathlib
159+
peername
159160
ping
160161
pipelining
161162
pluggable

docs/web_reference.rst

+15
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,21 @@ and :ref:`aiohttp-web-signals` handlers.
126126
Returns :class:`str`, or ``None`` if no host name is found in the
127127
headers.
128128

129+
.. attribute:: remote
130+
131+
Originating IP address of a client initiated HTTP request.
132+
133+
The IP is resolved through the following headers, in this order:
134+
135+
- *Forwarded*
136+
- *X-Forwarded-For*
137+
- peer name of opened socket
138+
139+
Returns :class:`str`, or ``None`` if no remote IP information is
140+
provided.
141+
142+
.. versionadded:: 2.3
143+
129144
.. attribute:: path_qs
130145

131146
The URL including PATH_INFO and the query string. e.g.,

tests/test_helpers.py

+20-31
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ def test_basic_auth_decode_bad_base64():
131131

132132

133133
def test_access_logger_format():
134-
log_format = '%T {%{SPAM}e} "%{ETag}o" %X {X} %%P %{FOO_TEST}e %{FOO1}e'
134+
log_format = '%T "%{ETag}o" %X {X} %%P'
135135
mock_logger = mock.Mock()
136136
access_logger = helpers.AccessLogger(mock_logger, log_format)
137-
expected = '%s {%s} "%s" %%X {X} %%%s %s %s'
137+
expected = '%s "%s" %%X {X} %%%s'
138138
assert expected == access_logger._log_format
139139

140140

@@ -147,12 +147,11 @@ def test_access_logger_atoms(mocker):
147147
log_format = '%a %t %P %l %u %r %s %b %T %Tf %D'
148148
mock_logger = mock.Mock()
149149
access_logger = helpers.AccessLogger(mock_logger, log_format)
150-
message = mock.Mock(headers={}, method="GET", path="/path", version=(1, 1))
151-
environ = {}
150+
request = mock.Mock(headers={}, method="GET", path_qs="/path",
151+
version=(1, 1),
152+
remote="127.0.0.2")
152153
response = mock.Mock(headers={}, body_length=42, status=200)
153-
transport = mock.Mock()
154-
transport.get_extra_info.return_value = ("127.0.0.2", 1234)
155-
access_logger.log(message, environ, response, transport, 3.1415926)
154+
access_logger.log(request, response, 3.1415926)
156155
assert not mock_logger.exception.called
157156
expected = ('127.0.0.2 [01/Jan/1843:00:00:00 +0000] <42> - - '
158157
'GET /path HTTP/1.1 200 42 3 3.141593 3141593')
@@ -171,19 +170,16 @@ def test_access_logger_atoms(mocker):
171170

172171

173172
def test_access_logger_dicts():
174-
log_format = '%{User-Agent}i %{Content-Length}o %{SPAM}e %{None}i'
173+
log_format = '%{User-Agent}i %{Content-Length}o %{None}i'
175174
mock_logger = mock.Mock()
176175
access_logger = helpers.AccessLogger(mock_logger, log_format)
177-
message = mock.Mock(headers={"User-Agent": "Mock/1.0"}, version=(1, 1))
178-
environ = {"SPAM": "EGGS"}
176+
request = mock.Mock(headers={"User-Agent": "Mock/1.0"}, version=(1, 1),
177+
remote="127.0.0.2")
179178
response = mock.Mock(headers={"Content-Length": 123})
180-
transport = mock.Mock()
181-
transport.get_extra_info.return_value = ("127.0.0.2", 1234)
182-
access_logger.log(message, environ, response, transport, 0.0)
179+
access_logger.log(request, response, 0.0)
183180
assert not mock_logger.error.called
184-
expected = 'Mock/1.0 123 EGGS -'
181+
expected = 'Mock/1.0 123 -'
185182
extra = {
186-
'environ': {'SPAM': 'EGGS'},
187183
'request_header': {'None': '-'},
188184
'response_header': {'Content-Length': 123}
189185
}
@@ -195,46 +191,39 @@ def test_access_logger_unix_socket():
195191
log_format = '|%a|'
196192
mock_logger = mock.Mock()
197193
access_logger = helpers.AccessLogger(mock_logger, log_format)
198-
message = mock.Mock(headers={"User-Agent": "Mock/1.0"}, version=(1, 1))
199-
environ = {}
194+
request = mock.Mock(headers={"User-Agent": "Mock/1.0"}, version=(1, 1),
195+
remote="")
200196
response = mock.Mock()
201-
transport = mock.Mock()
202-
transport.get_extra_info.return_value = ""
203-
access_logger.log(message, environ, response, transport, 0.0)
197+
access_logger.log(request, response, 0.0)
204198
assert not mock_logger.error.called
205199
expected = '||'
206200
mock_logger.info.assert_called_with(expected, extra={'remote_address': ''})
207201

208202

209-
def test_logger_no_message_and_environ():
203+
def test_logger_no_message():
210204
mock_logger = mock.Mock()
211-
mock_transport = mock.Mock()
212-
mock_transport.get_extra_info.return_value = ("127.0.0.3", 0)
213205
access_logger = helpers.AccessLogger(mock_logger,
214-
"%r %{FOOBAR}e %{content-type}i")
206+
"%r %{content-type}i")
215207
extra_dict = {
216-
'environ': {'FOOBAR': '-'},
217208
'first_request_line': '-',
218209
'request_header': {'content-type': '(no headers)'}
219210
}
220211

221-
access_logger.log(None, None, None, mock_transport, 0.0)
222-
mock_logger.info.assert_called_with("- - (no headers)", extra=extra_dict)
212+
access_logger.log(None, None, 0.0)
213+
mock_logger.info.assert_called_with("- (no headers)", extra=extra_dict)
223214

224215

225216
def test_logger_internal_error():
226217
mock_logger = mock.Mock()
227-
mock_transport = mock.Mock()
228-
mock_transport.get_extra_info.return_value = ("127.0.0.3", 0)
229218
access_logger = helpers.AccessLogger(mock_logger, "%D")
230-
access_logger.log(None, None, None, mock_transport, 'invalid')
219+
access_logger.log(None, None, 'invalid')
231220
mock_logger.exception.assert_called_with("Error in logging")
232221

233222

234223
def test_logger_no_transport():
235224
mock_logger = mock.Mock()
236225
access_logger = helpers.AccessLogger(mock_logger, "%a")
237-
access_logger.log(None, None, None, None, 0)
226+
access_logger.log(None, None, 0)
238227
mock_logger.info.assert_called_with("-", extra={'remote_address': '-'})
239228

240229

0 commit comments

Comments
 (0)