Skip to content

Commit 7c9b7b9

Browse files
authored
[PR #9822/249df6e backport][3.11] Improve performance of the access logger when its disabled (#9828)
1 parent 9adc6c1 commit 7c9b7b9

File tree

7 files changed

+67
-6
lines changed

7 files changed

+67
-6
lines changed

CHANGES/9822.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added an :attr:`~aiohttp.abc.AbstractAccessLogger.enabled` property to :class:`aiohttp.abc.AbstractAccessLogger` to dynamically check if logging is enabled -- by :user:`bdraco`.

aiohttp/abc.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,17 @@ async def write_headers(
231231
class AbstractAccessLogger(ABC):
232232
"""Abstract writer to access log."""
233233

234+
__slots__ = ("logger", "log_format")
235+
234236
def __init__(self, logger: logging.Logger, log_format: str) -> None:
235237
self.logger = logger
236238
self.log_format = log_format
237239

238240
@abstractmethod
239241
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
240242
"""Emit log to logger."""
243+
244+
@property
245+
def enabled(self) -> bool:
246+
"""Check if logger is enabled."""
247+
return True

aiohttp/web_log.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,13 @@ def _format_line(
188188
) -> Iterable[Tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]:
189189
return [(key, method(request, response, time)) for key, method in self._methods]
190190

191+
@property
192+
def enabled(self) -> bool:
193+
"""Check if logger is enabled."""
194+
# Avoid formatting the log line if it will not be emitted.
195+
return self.logger.isEnabledFor(logging.INFO)
196+
191197
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
192-
if not self.logger.isEnabledFor(logging.INFO):
193-
# Avoid formatting the log line if it will not be emitted.
194-
return
195198
try:
196199
fmt_info = self._format_line(request, response, time)
197200

aiohttp/web_protocol.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ def force_close(self) -> None:
438438
def log_access(
439439
self, request: BaseRequest, response: StreamResponse, time: float
440440
) -> None:
441-
if self.access_logger is not None:
441+
if self.access_logger is not None and self.access_logger.enabled:
442442
self.access_logger.log(request, response, self._loop.time() - time)
443443

444444
def log_debug(self, *args: Any, **kw: Any) -> None:

docs/abc.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ Abstract Access Logger
182182

183183
:param float time: Time taken to serve the request.
184184

185+
.. attribute:: enabled
186+
187+
Return True if logger is enabled.
188+
189+
Override this property if logging is disabled to avoid the
190+
overhead of calculating details to feed the logger.
191+
192+
This property may be omitted if logging is always enabled.
193+
185194

186195
Abstract Resolver
187196
-------------------------------

docs/logging.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ Example of a drop-in replacement for the default access logger::
115115
f'"{request.method} {request.path} '
116116
f'done in {time}s: {response.status}')
117117

118+
@property
119+
def enabled(self):
120+
"""Return True if logger is enabled.
121+
122+
Override this property if logging is disabled to avoid the
123+
overhead of calculating details to feed the logger.
124+
125+
This property may be omitted if logging is always enabled.
126+
"""
127+
return self.logger.isEnabledFor(logging.INFO)
118128

119129
.. _gunicorn-accesslog:
120130

tests/test_web_log.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import aiohttp
1010
from aiohttp import web
1111
from aiohttp.abc import AbstractAccessLogger
12+
from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer
1213
from aiohttp.typedefs import Handler
1314
from aiohttp.web_log import AccessLogger
1415

@@ -215,12 +216,42 @@ def log(self, request, response, time):
215216
assert msg == "contextvars: uuid"
216217

217218

218-
def test_logger_does_nothing_when_disabled(caplog: pytest.LogCaptureFixture) -> None:
219-
"""Test that the logger does nothing when the log level is disabled."""
219+
def test_access_logger_feeds_logger(caplog: pytest.LogCaptureFixture) -> None:
220+
"""Test that the logger still works."""
220221
mock_logger = logging.getLogger("test.aiohttp.log")
221222
mock_logger.setLevel(logging.INFO)
222223
access_logger = AccessLogger(mock_logger, "%b")
223224
access_logger.log(
224225
mock.Mock(name="mock_request"), mock.Mock(name="mock_response"), 42
225226
)
226227
assert "mock_response" in caplog.text
228+
229+
230+
async def test_logger_does_not_log_when_not_enabled(
231+
aiohttp_server: AiohttpServer,
232+
aiohttp_client: AiohttpClient,
233+
caplog: pytest.LogCaptureFixture,
234+
) -> None:
235+
"""Test logger does nothing when not enabled."""
236+
237+
async def handler(request: web.Request) -> web.Response:
238+
return web.Response()
239+
240+
class Logger(AbstractAccessLogger):
241+
242+
def log(
243+
self, request: web.BaseRequest, response: web.StreamResponse, time: float
244+
) -> None:
245+
self.logger.critical("This should not be logged") # pragma: no cover
246+
247+
@property
248+
def enabled(self) -> bool:
249+
return False
250+
251+
app = web.Application()
252+
app.router.add_get("/", handler)
253+
server = await aiohttp_server(app, access_log_class=Logger)
254+
client = await aiohttp_client(server)
255+
resp = await client.get("/")
256+
assert 200 == resp.status
257+
assert "This should not be logged" not in caplog.text

0 commit comments

Comments
 (0)