Add ability to log all exceptions (#2262)

* Add ability to log all exceptions

* Fix linting 🙄

* Remove shorthand

* Make `ErrorHandler.log` backwards-compat

* Ignore mypy error

* Don't store `noisy_exceptions` attribute in app

* Added tests

* Store noisy exceptions setting in config

* Default to not-noisy if config key not available

* Add CLI tests for `noisy-exceptions`

* Remove debugging line I left in 😅

* Fix tests

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
This commit is contained in:
Néstor Pérez 2021-10-27 09:43:58 +02:00 committed by GitHub
parent 71cc30e5cd
commit f0f81ec458
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 2 deletions

View File

@ -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

View File

@ -962,6 +962,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
: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 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
: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 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
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,

View File

@ -27,6 +27,7 @@ DEFAULT_CONFIG = {
"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

View File

@ -192,7 +192,8 @@ class ErrorHandler:
@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:

View File

@ -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))

View File

@ -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

View File

@ -2,10 +2,11 @@ import asyncio
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 test_single_arg_exception_handler_notice(exception_handler_app, caplog):
"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)
)