Skip to content

Commit

Permalink
implementing AbstractAsyncAccessLogger (aio-libs#3767)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin authored and asvetlov committed May 23, 2019
1 parent 6b0a651 commit d73e5eb
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGES/3767.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``AbstractAsyncAccessLogger`` to allow IO while logging.
11 changes: 11 additions & 0 deletions aiohttp/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,14 @@ def log(self,
response: StreamResponse,
time: float) -> None:
"""Emit log to logger."""


class AbstractAsyncAccessLogger(ABC):
"""Abstract asynchronous writer to access log."""

@abstractmethod
async def log(self,
request: BaseRequest,
response: StreamResponse,
request_start: float) -> None:
"""Emit log to logger."""
52 changes: 40 additions & 12 deletions aiohttp/web_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
Callable,
Optional,
Type,
Union,
cast,
)

import yarl

from .abc import AbstractAccessLogger, AbstractStreamWriter
from .abc import (
AbstractAccessLogger,
AbstractAsyncAccessLogger,
AbstractStreamWriter,
)
from .base_protocol import BaseProtocol
from .helpers import CeilTimeout, current_task
from .http import (
Expand Down Expand Up @@ -50,6 +55,10 @@
BaseRequest]

_RequestHandler = Callable[[BaseRequest], Awaitable[StreamResponse]]
_AnyAbstractAccessLogger = Union[
Type[AbstractAsyncAccessLogger],
Type[AbstractAccessLogger],
]


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


class AccessLoggerWrapper(AbstractAsyncAccessLogger):
"""
Wraps an AbstractAccessLogger so it behaves
like an AbstractAsyncAccessLogger.
"""
def __init__(self, access_logger: AbstractAccessLogger):
self.access_logger = access_logger
super().__init__()

async def log(self,
request: BaseRequest,
response: StreamResponse,
request_start: float) -> None:
self.access_logger.log(request, response, request_start)


class RequestHandler(BaseProtocol):
"""HTTP protocol implementation.
Expand Down Expand Up @@ -120,7 +145,7 @@ def __init__(self, manager: 'Server', *,
keepalive_timeout: float=75., # NGINX default is 75 secs
tcp_keepalive: bool=True,
logger: Logger=server_logger,
access_log_class: Type[AbstractAccessLogger]=AccessLogger,
access_log_class: _AnyAbstractAccessLogger=AccessLogger,
access_log: Logger=access_logger,
access_log_format: str=AccessLogger.LOG_FORMAT,
debug: bool=False,
Expand Down Expand Up @@ -164,8 +189,11 @@ def __init__(self, manager: 'Server', *,
self.debug = debug
self.access_log = access_log
if access_log:
self.access_logger = access_log_class(
access_log, access_log_format) # type: Optional[AbstractAccessLogger] # noqa
if issubclass(access_log_class, AbstractAsyncAccessLogger):
self.access_logger = access_log_class() # type: Optional[AbstractAsyncAccessLogger] # noqa
else:
access_logger = access_log_class(access_log, access_log_format)
self.access_logger = AccessLoggerWrapper(access_logger)
else:
self.access_logger = None

Expand Down Expand Up @@ -339,13 +367,13 @@ def force_close(self) -> None:
self.transport.close()
self.transport = None

def log_access(self,
request: BaseRequest,
response: StreamResponse,
request_start: float) -> None:
async def log_access(self,
request: BaseRequest,
response: StreamResponse,
request_start: float) -> None:
if self.access_logger is not None:
self.access_logger.log(request, response,
self._loop.time() - request_start)
await self.access_logger.log(request, response,
self._loop.time() - request_start)

def log_debug(self, *args: Any, **kw: Any) -> None:
if self.debug:
Expand Down Expand Up @@ -526,10 +554,10 @@ async def finish_response(self,
await prepare_meth(request)
await resp.write_eof()
except ConnectionResetError:
self.log_access(request, resp, start_time)
await self.log_access(request, resp, start_time)
return True
else:
self.log_access(request, resp, start_time)
await self.log_access(request, resp, start_time)
return False

def handle_error(self,
Expand Down
22 changes: 22 additions & 0 deletions docs/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ Example of a drop-in replacement for the default access logger::
``AccessLogger.log()`` can now access any exception raised while processing
the request with ``sys.exc_info()``.


.. versionadded:: 4.0.0


If your logging needs to perform IO you can instead inherit from
:class:`aiohttp.abc.AbstractAsyncAccessLogger`::


from aiohttp.abc import AbstractAsyncAccessLogger

class AccessLogger(AbstractAsyncAccessLogger):

async def log(self, request, response, time):
logging_service = request.app['logging_service']
await logging_service.log(f'{request.remote} '
f'"{request.method} {request.path} '
f'done in {time}s: {response.status}')


This also allows access to the results of coroutines on the ``request`` and
``response``, e.g. ``request.text()``.

.. _gunicorn-accesslog:

Gunicorn access logs
Expand Down
24 changes: 23 additions & 1 deletion tests/test_web_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import pytest

from aiohttp.abc import AbstractAccessLogger
from aiohttp.abc import AbstractAccessLogger, AbstractAsyncAccessLogger
from aiohttp.web_log import AccessLogger
from aiohttp.web_response import Response

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

Expand Down Expand Up @@ -178,3 +179,24 @@ async def handler(request):
resp = await cli.get('/path/to', headers={'Accept': 'text/html'})
assert resp.status == 500
assert exc_msg == 'RuntimeError: intentional runtime error'


async def test_async_logger(aiohttp_raw_server, aiohttp_client):
msg = None

class Logger(AbstractAsyncAccessLogger):
async def log(self, request, response, time):
nonlocal msg
msg = '{}: {}'.format(request.path, response.status)

async def handler(request):
return Response(text='ok')

logger = mock.Mock()
server = await aiohttp_raw_server(handler,
access_log_class=Logger,
logger=logger)
cli = await aiohttp_client(server)
resp = await cli.get('/path/to', headers={'Accept': 'text/html'})
assert resp.status == 200
assert msg == '/path/to: 200'

0 comments on commit d73e5eb

Please sign in to comment.