Skip to content

Commit d73e5eb

Browse files
samuelcolvinasvetlov
authored andcommitted
implementing AbstractAsyncAccessLogger (aio-libs#3767)
1 parent 6b0a651 commit d73e5eb

File tree

5 files changed

+97
-13
lines changed

5 files changed

+97
-13
lines changed

CHANGES/3767.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ``AbstractAsyncAccessLogger`` to allow IO while logging.

aiohttp/abc.py

+11
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,14 @@ def log(self,
206206
response: StreamResponse,
207207
time: float) -> None:
208208
"""Emit log to logger."""
209+
210+
211+
class AbstractAsyncAccessLogger(ABC):
212+
"""Abstract asynchronous writer to access log."""
213+
214+
@abstractmethod
215+
async def log(self,
216+
request: BaseRequest,
217+
response: StreamResponse,
218+
request_start: float) -> None:
219+
"""Emit log to logger."""

aiohttp/web_protocol.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
Callable,
1414
Optional,
1515
Type,
16+
Union,
1617
cast,
1718
)
1819

1920
import yarl
2021

21-
from .abc import AbstractAccessLogger, AbstractStreamWriter
22+
from .abc import (
23+
AbstractAccessLogger,
24+
AbstractAsyncAccessLogger,
25+
AbstractStreamWriter,
26+
)
2227
from .base_protocol import BaseProtocol
2328
from .helpers import CeilTimeout, current_task
2429
from .http import (
@@ -50,6 +55,10 @@
5055
BaseRequest]
5156

5257
_RequestHandler = Callable[[BaseRequest], Awaitable[StreamResponse]]
58+
_AnyAbstractAccessLogger = Union[
59+
Type[AbstractAsyncAccessLogger],
60+
Type[AbstractAccessLogger],
61+
]
5362

5463

5564
ERROR = RawRequestMessage(
@@ -65,6 +74,22 @@ class PayloadAccessError(Exception):
6574
"""Payload was accessed after response was sent."""
6675

6776

77+
class AccessLoggerWrapper(AbstractAsyncAccessLogger):
78+
"""
79+
Wraps an AbstractAccessLogger so it behaves
80+
like an AbstractAsyncAccessLogger.
81+
"""
82+
def __init__(self, access_logger: AbstractAccessLogger):
83+
self.access_logger = access_logger
84+
super().__init__()
85+
86+
async def log(self,
87+
request: BaseRequest,
88+
response: StreamResponse,
89+
request_start: float) -> None:
90+
self.access_logger.log(request, response, request_start)
91+
92+
6893
class RequestHandler(BaseProtocol):
6994
"""HTTP protocol implementation.
7095
@@ -120,7 +145,7 @@ def __init__(self, manager: 'Server', *,
120145
keepalive_timeout: float=75., # NGINX default is 75 secs
121146
tcp_keepalive: bool=True,
122147
logger: Logger=server_logger,
123-
access_log_class: Type[AbstractAccessLogger]=AccessLogger,
148+
access_log_class: _AnyAbstractAccessLogger=AccessLogger,
124149
access_log: Logger=access_logger,
125150
access_log_format: str=AccessLogger.LOG_FORMAT,
126151
debug: bool=False,
@@ -164,8 +189,11 @@ def __init__(self, manager: 'Server', *,
164189
self.debug = debug
165190
self.access_log = access_log
166191
if access_log:
167-
self.access_logger = access_log_class(
168-
access_log, access_log_format) # type: Optional[AbstractAccessLogger] # noqa
192+
if issubclass(access_log_class, AbstractAsyncAccessLogger):
193+
self.access_logger = access_log_class() # type: Optional[AbstractAsyncAccessLogger] # noqa
194+
else:
195+
access_logger = access_log_class(access_log, access_log_format)
196+
self.access_logger = AccessLoggerWrapper(access_logger)
169197
else:
170198
self.access_logger = None
171199

@@ -339,13 +367,13 @@ def force_close(self) -> None:
339367
self.transport.close()
340368
self.transport = None
341369

342-
def log_access(self,
343-
request: BaseRequest,
344-
response: StreamResponse,
345-
request_start: float) -> None:
370+
async def log_access(self,
371+
request: BaseRequest,
372+
response: StreamResponse,
373+
request_start: float) -> None:
346374
if self.access_logger is not None:
347-
self.access_logger.log(request, response,
348-
self._loop.time() - request_start)
375+
await self.access_logger.log(request, response,
376+
self._loop.time() - request_start)
349377

350378
def log_debug(self, *args: Any, **kw: Any) -> None:
351379
if self.debug:
@@ -526,10 +554,10 @@ async def finish_response(self,
526554
await prepare_meth(request)
527555
await resp.write_eof()
528556
except ConnectionResetError:
529-
self.log_access(request, resp, start_time)
557+
await self.log_access(request, resp, start_time)
530558
return True
531559
else:
532-
self.log_access(request, resp, start_time)
560+
await self.log_access(request, resp, start_time)
533561
return False
534562

535563
def handle_error(self,

docs/logging.rst

+22
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@ Example of a drop-in replacement for the default access logger::
111111
``AccessLogger.log()`` can now access any exception raised while processing
112112
the request with ``sys.exc_info()``.
113113

114+
115+
.. versionadded:: 4.0.0
116+
117+
118+
If your logging needs to perform IO you can instead inherit from
119+
:class:`aiohttp.abc.AbstractAsyncAccessLogger`::
120+
121+
122+
from aiohttp.abc import AbstractAsyncAccessLogger
123+
124+
class AccessLogger(AbstractAsyncAccessLogger):
125+
126+
async def log(self, request, response, time):
127+
logging_service = request.app['logging_service']
128+
await logging_service.log(f'{request.remote} '
129+
f'"{request.method} {request.path} '
130+
f'done in {time}s: {response.status}')
131+
132+
133+
This also allows access to the results of coroutines on the ``request`` and
134+
``response``, e.g. ``request.text()``.
135+
114136
.. _gunicorn-accesslog:
115137

116138
Gunicorn access logs

tests/test_web_log.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import pytest
77

8-
from aiohttp.abc import AbstractAccessLogger
8+
from aiohttp.abc import AbstractAccessLogger, AbstractAsyncAccessLogger
99
from aiohttp.web_log import AccessLogger
10+
from aiohttp.web_response import Response
1011

1112
IS_PYPY = platform.python_implementation() == 'PyPy'
1213

@@ -178,3 +179,24 @@ async def handler(request):
178179
resp = await cli.get('/path/to', headers={'Accept': 'text/html'})
179180
assert resp.status == 500
180181
assert exc_msg == 'RuntimeError: intentional runtime error'
182+
183+
184+
async def test_async_logger(aiohttp_raw_server, aiohttp_client):
185+
msg = None
186+
187+
class Logger(AbstractAsyncAccessLogger):
188+
async def log(self, request, response, time):
189+
nonlocal msg
190+
msg = '{}: {}'.format(request.path, response.status)
191+
192+
async def handler(request):
193+
return Response(text='ok')
194+
195+
logger = mock.Mock()
196+
server = await aiohttp_raw_server(handler,
197+
access_log_class=Logger,
198+
logger=logger)
199+
cli = await aiohttp_client(server)
200+
resp = await cli.get('/path/to', headers={'Accept': 'text/html'})
201+
assert resp.status == 200
202+
assert msg == '/path/to: 200'

0 commit comments

Comments
 (0)