diff --git a/sanic/__main__.py b/sanic/__main__.py index 027bf8793d..7903c7285e 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -96,6 +96,11 @@ def main(): help="number of worker processes [default 1]\n ", ) parser.add_argument("-d", "--debug", dest="debug", action="store_true") + parser.add_bool_arguments( + "--noisy-exceptions", + dest="noisy_exceptions", + help="print stack traces for all exceptions", + ) parser.add_argument( "-r", "--reload", @@ -149,6 +154,7 @@ def main(): f"Module is not a Sanic app, it is a {app_type_name}. " f"Perhaps you meant {args.module}.app?" ) + if args.cert is not None or args.key is not None: ssl: Optional[Dict[str, Any]] = { "cert": args.cert, @@ -165,7 +171,9 @@ def main(): "debug": args.debug, "access_log": args.access_log, "ssl": ssl, + "noisy_exceptions": args.noisy_exceptions, } + if args.auto_reload: kwargs["auto_reload"] = True diff --git a/sanic/app.py b/sanic/app.py index 3cacdf6c5c..15e87111d1 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -962,6 +962,7 @@ def run( unix: Optional[str] = None, loop: None = None, reload_dir: Optional[Union[List[str], str]] = None, + noisy_exceptions: Optional[bool] = None, ) -> None: """ Run the HTTP Server and listen until keyboard interrupt or term @@ -994,6 +995,9 @@ def run( :type access_log: bool :param unix: Unix socket to listen on instead of TCP port :type unix: str + :param noisy_exceptions: Log exceptions that are normally considered + to be quiet/silent + :type noisy_exceptions: bool :return: Nothing """ if reload_dir: @@ -1032,6 +1036,9 @@ def run( if access_log is not None: self.config.ACCESS_LOG = access_log + if noisy_exceptions is not None: + self.config.NOISY_EXCEPTIONS = noisy_exceptions + server_settings = self._helper( host=host, port=port, @@ -1090,6 +1097,7 @@ async def create_server( unix: Optional[str] = None, return_asyncio_server: bool = False, asyncio_server_kwargs: Dict[str, Any] = None, + noisy_exceptions: Optional[bool] = None, ) -> Optional[AsyncioServer]: """ Asynchronous version of :func:`run`. @@ -1127,6 +1135,9 @@ async def create_server( :param asyncio_server_kwargs: key-value arguments for asyncio/uvloop create_server method :type asyncio_server_kwargs: dict + :param noisy_exceptions: Log exceptions that are normally considered + to be quiet/silent + :type noisy_exceptions: bool :return: AsyncioServer if return_asyncio_server is true, else Nothing """ @@ -1137,10 +1148,14 @@ async def create_server( protocol = ( WebSocketProtocol if self.websocket_enabled else HttpProtocol ) + # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: self.config.ACCESS_LOG = access_log + if noisy_exceptions is not None: + self.config.NOISY_EXCEPTIONS = noisy_exceptions + server_settings = self._helper( host=host, port=port, diff --git a/sanic/config.py b/sanic/config.py index 649d9414bc..1b406a4311 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -27,6 +27,7 @@ "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds "KEEP_ALIVE": True, + "NOISY_EXCEPTIONS": False, "PROXIES_COUNT": None, "REAL_IP_HEADER": None, "REGISTER": True, @@ -51,6 +52,7 @@ class Config(dict): GRACEFUL_SHUTDOWN_TIMEOUT: float KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE: bool + NOISY_EXCEPTIONS: bool PROXIES_COUNT: Optional[int] REAL_IP_HEADER: Optional[str] REGISTER: bool diff --git a/sanic/handlers.py b/sanic/handlers.py index fd718f06e4..af667c9a8e 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -192,7 +192,8 @@ def default(self, request, exception): @staticmethod def log(request, exception): quiet = getattr(exception, "quiet", False) - if quiet is False: + noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False) + if quiet is False or noisy is True: try: url = repr(request.url) except AttributeError: diff --git a/tests/fake/server.py b/tests/fake/server.py index 9c28f54a29..43f6d27faf 100644 --- a/tests/fake/server.py +++ b/tests/fake/server.py @@ -23,6 +23,7 @@ async def app_info_dump(app: Sanic, _): "access_log": app.config.ACCESS_LOG, "auto_reload": app.auto_reload, "debug": app.debug, + "noisy_exceptions": app.config.NOISY_EXCEPTIONS, } logger.info(json.dumps(app_data)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 43efbb2692..4e386c529f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -182,3 +182,21 @@ def test_version(cmd): version_string = f"Sanic {__version__}; Routing {__routing_version__}\n" assert out == version_string.encode("utf-8") + + +@pytest.mark.parametrize( + "cmd,expected", + ( + ("--noisy-exceptions", True), + ("--no-noisy-exceptions", False), + ), +) +def test_noisy_exceptions(cmd, expected): + command = ["sanic", "fake.server.app", cmd] + out, err, exitcode = capture(command) + lines = out.split(b"\n") + + app_info = lines[26] + info = json.loads(app_info) + + assert info["noisy_exceptions"] is expected diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 9bedf7e67c..edc5a32706 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -2,10 +2,11 @@ import logging import pytest +from unittest.mock import Mock from bs4 import BeautifulSoup -from sanic import Sanic +from sanic import Sanic, handlers from sanic.exceptions import Forbidden, InvalidUsage, NotFound, ServerError from sanic.handlers import ErrorHandler from sanic.response import stream, text @@ -227,3 +228,18 @@ def lookup(self, exception): "v22.3, the legacy style lookup method will not work at all." ) assert response.status == 400 + + +def test_error_handler_noisy_log(exception_handler_app, monkeypatch): + err_logger = Mock() + monkeypatch.setattr(handlers, "error_logger", err_logger) + + exception_handler_app.config["NOISY_EXCEPTIONS"] = False + exception_handler_app.test_client.get("/1") + err_logger.exception.assert_not_called() + + exception_handler_app.config["NOISY_EXCEPTIONS"] = True + request, _ = exception_handler_app.test_client.get("/1") + err_logger.exception.assert_called_with( + "Exception occurred while handling uri: %s", repr(request.url) + )