Repository: aio-libs/aiohttp

Issue:
Enable stdout logging in run_app by default
Flask adds StreamHandler to application logger if the logger has no handlers at the moment of application startup.
We should do the same in web.run_app. AppRunner should be configured explicitly.

http://flask.pocoo.org/docs/1.0/logging/#default-configuration

See https://github.com/aio-libs/aiohttp/issues/2952#issuecomment-418994782 for motivation
Enable stdout logging in run_app by default
Flask adds StreamHandler to application logger if the logger has no handlers at the moment of application startup.
We should do the same in web.run_app. AppRunner should be configured explicitly.

http://flask.pocoo.org/docs/1.0/logging/#default-configuration

See https://github.com/aio-libs/aiohttp/issues/2952#issuecomment-418994782 for motivation


Discussion:
The fix is relatively easy.
Mind if I have a go at this?

If I understand this issue correctly, all that needs to be done is add a check at the start of `run_app` in [web.py](https://github.com/aio-libs/aiohttp/blob/master/aiohttp/web.py#L43) before the AppRunner is created. Is this correct?
Please do.
Your understanding is correct.
The fix is relatively easy.
Mind if I have a go at this?

If I understand this issue correctly, all that needs to be done is add a check at the start of `run_app` in [web.py](https://github.com/aio-libs/aiohttp/blob/master/aiohttp/web.py#L43) before the AppRunner is created. Is this correct?
Please do.
Your understanding is correct.

Resolving patch:
diff --git a/aiohttp/web.py b/aiohttp/web.py
--- a/aiohttp/web.py
+++ b/aiohttp/web.py
@@ -52,6 +52,13 @@ def run_app(app, *, host=None, port=None, path=None, sock=None,
     if asyncio.iscoroutine(app):
         app = loop.run_until_complete(app)
 
+    # Configure if and only if in debugging mode and using the default logger
+    if app.debug and access_log.name == 'aiohttp.access':
+        if access_log.level == logging.NOTSET:
+            access_log.setLevel(logging.DEBUG)
+        if not access_log.hasHandlers():
+            access_log.addHandler(logging.StreamHandler())
+
     runner = AppRunner(app, handle_signals=handle_signals,
                        access_log_class=access_log_class,
                        access_log_format=access_log_format,


Test patch:
diff --git a/tests/test_run_app.py b/tests/test_run_app.py
--- a/tests/test_run_app.py
+++ b/tests/test_run_app.py
@@ -1,5 +1,6 @@
 import asyncio
 import contextlib
+import logging
 import os
 import platform
 import signal
@@ -539,3 +540,80 @@ async def make_app():
                                                   reuse_port=None)
     startup_handler.assert_called_once_with(mock.ANY)
     cleanup_handler.assert_called_once_with(mock.ANY)
+
+
+def test_run_app_default_logger(monkeypatch, patched_loop):
+    logger = web.access_logger
+    attrs = {
+        'hasHandlers.return_value': False,
+        'level': logging.NOTSET,
+        'name': 'aiohttp.access',
+    }
+    mock_logger = mock.create_autospec(logger, name='mock_access_logger')
+    mock_logger.configure_mock(**attrs)
+
+    app = web.Application(debug=True)
+    web.run_app(app,
+                print=stopper(patched_loop),
+                access_log=mock_logger)
+    mock_logger.setLevel.assert_any_call(logging.DEBUG)
+    mock_logger.hasHandlers.assert_called_with()
+    assert isinstance(mock_logger.addHandler.call_args[0][0],
+                      logging.StreamHandler)
+
+
+def test_run_app_default_logger_setup_requires_debug(patched_loop):
+    logger = web.access_logger
+    attrs = {
+        'hasHandlers.return_value': False,
+        'level': logging.NOTSET,
+        'name': 'aiohttp.access',
+    }
+    mock_logger = mock.create_autospec(logger, name='mock_access_logger')
+    mock_logger.configure_mock(**attrs)
+
+    app = web.Application(debug=False)
+    web.run_app(app,
+                print=stopper(patched_loop),
+                access_log=mock_logger)
+    mock_logger.setLevel.assert_not_called()
+    mock_logger.hasHandlers.assert_not_called()
+    mock_logger.addHandler.assert_not_called()
+
+
+def test_run_app_default_logger_setup_requires_default_logger(patched_loop):
+    logger = web.access_logger
+    attrs = {
+        'hasHandlers.return_value': False,
+        'level': logging.NOTSET,
+        'name': None,
+    }
+    mock_logger = mock.create_autospec(logger, name='mock_access_logger')
+    mock_logger.configure_mock(**attrs)
+
+    app = web.Application()
+    web.run_app(app,
+                print=stopper(patched_loop),
+                access_log=mock_logger)
+    mock_logger.setLevel.assert_not_called()
+    mock_logger.hasHandlers.assert_not_called()
+    mock_logger.addHandler.assert_not_called()
+
+
+def test_run_app_default_logger_setup_only_if_unconfigured(patched_loop):
+    logger = web.access_logger
+    attrs = {
+        'hasHandlers.return_value': True,
+        'level': None,
+        'name': 'aiohttp.access',
+    }
+    mock_logger = mock.create_autospec(logger, name='mock_access_logger')
+    mock_logger.configure_mock(**attrs)
+
+    app = web.Application(debug=False)
+    web.run_app(app,
+                print=stopper(patched_loop),
+                access_log=mock_logger)
+    mock_logger.setLevel.assert_not_called()
+    mock_logger.hasHandlers.assert_not_called()
+    mock_logger.addHandler.assert_not_called()