This commit is contained in:
Adrian 2026-08-15 11:16:48 -05:00 committed by GitHub
commit 64c89bae1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 13 deletions

View File

@ -387,12 +387,13 @@ is ``True``.
Logging settings
----------------
**Logging settings** are settings that configure the global root logging
handler installed by :func:`~scrapy.utils.log.configure_logging`.
**Logging settings** are settings that configure logging process-wide, mostly
through the global root logging handler installed by
:func:`~scrapy.utils.log.configure_logging`.
These settings can be defined from a spider. However, because only 1 root
logging handler is active per process, these settings cannot use a different
value per spider when :ref:`running multiple spiders in the same process
These settings can be defined from a spider. However, because logging
configuration is global, these settings cannot use a different value per
spider when :ref:`running multiple spiders in the same process
<run-multiple-spiders>`.
These settings are:
@ -404,6 +405,7 @@ These settings are:
- :setting:`LOG_FILE_APPEND`
- :setting:`LOG_FORMAT`
- :setting:`LOG_LEVEL`
- :setting:`LOG_LEVELS`
- :setting:`LOG_SHORT_NAMES`
- :setting:`LOG_STDOUT`
@ -1670,6 +1672,30 @@ INFO, DEBUG. For more info see :ref:`topics-logging`.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_LEVELS
LOG_LEVELS
----------
Default: ``{}``
.. versionadded:: VERSION
Minimum level to log for specific loggers.
It takes precedence over the levels that Scrapy sets by default::
{
"filelock": "ERROR",
"hpack": "ERROR",
"httpcore": "ERROR",
"httpx": "WARNING",
"scrapy": "DEBUG",
"twisted": "ERROR",
}
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_STDOUT
LOG_STDOUT

View File

@ -22,6 +22,7 @@ from scrapy.signalmanager import SignalManager
from scrapy.spiderloader import SpiderLoaderProtocol, get_spider_loader
from scrapy.utils.defer import deferred_from_coro
from scrapy.utils.log import (
_configure_logger_levels,
configure_logging,
get_scrapy_root_handler,
install_scrapy_root_handler,
@ -122,7 +123,7 @@ class Crawler:
self.spidercls: type[Spider] = spidercls
self.settings: Settings = settings.copy()
self.spidercls.update_settings(self.settings)
self._update_root_log_handler()
self._update_logging()
self.addons: AddonManager = AddonManager(self)
self.signals: SignalManager = SignalManager(self)
@ -139,9 +140,10 @@ class Crawler:
self._request_fingerprinter: RequestFingerprinterProtocol | None = None
self._stats: StatsCollector | None = None
def _update_root_log_handler(self) -> None:
def _update_logging(self) -> None:
if get_scrapy_root_handler() is not None:
# scrapy root handler already installed: update it with new settings
_configure_logger_levels(self.settings)
install_scrapy_root_handler(self.settings)
def _apply_settings(self) -> None:
@ -268,7 +270,7 @@ class Crawler:
try:
self.spider = self._create_spider(*args, **kwargs)
self._apply_settings()
self._update_root_log_handler()
self._update_logging()
self.engine = self._create_engine()
try:
yield deferred_from_coro(self.engine.open_spider_async())
@ -302,7 +304,7 @@ class Crawler:
try:
self.spider = self._create_spider(*args, **kwargs)
self._apply_settings()
self._update_root_log_handler()
self._update_logging()
self.engine = self._create_engine()
try:
await self.engine.open_spider_async()

View File

@ -145,6 +145,7 @@ __all__ = [
"LOG_FORMAT",
"LOG_FORMATTER",
"LOG_LEVEL",
"LOG_LEVELS",
"LOG_SHORT_NAMES",
"LOG_STDOUT",
"LOG_VERSIONS",
@ -449,6 +450,7 @@ LOG_FILE_APPEND = True
LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
LOG_FORMATTER = "scrapy.logformatter.LogFormatter"
LOG_LEVEL = "DEBUG"
LOG_LEVELS: dict[str, str] = {}
LOG_SHORT_NAMES = False
LOG_STDOUT = False
LOG_VERSIONS = [

View File

@ -65,7 +65,7 @@ class TopLevelFormatter(logging.Filter):
return True
DEFAULT_LOGGING = {
DEFAULT_LOGGING: dict[str, Any] = {
"version": 1,
"disable_existing_loggers": False,
"loggers": {
@ -109,7 +109,8 @@ def configure_logging(
This function does:
- Route warnings and twisted logging through Python standard logging
- Assign DEBUG and ERROR level to Scrapy and Twisted loggers respectively
- Set log levels for the Scrapy, Twisted and third-party loggers, which
:setting:`LOG_LEVELS` can override
- Route stdout to log if LOG_STDOUT setting is True
When ``install_root_handler`` is True (default), this function also
@ -125,11 +126,11 @@ def configure_logging(
observer = twisted_log.PythonLoggingObserver("twisted")
observer.start()
dictConfig(DEFAULT_LOGGING)
if isinstance(settings, dict) or settings is None:
settings = Settings(settings)
_configure_logger_levels(settings)
if settings.getbool("LOG_STDOUT"):
sys.stdout = StreamLogger(logging.getLogger("stdout"))
@ -137,6 +138,13 @@ def configure_logging(
install_scrapy_root_handler(settings)
def _configure_logger_levels(settings: Settings) -> None:
loggers = DEFAULT_LOGGING["loggers"] | {
name: {"level": level} for name, level in settings.getdict("LOG_LEVELS").items()
}
dictConfig(DEFAULT_LOGGING | {"loggers": loggers})
_scrapy_root_handler: logging.Handler | None = None

View File

@ -553,6 +553,20 @@ class TestCrawlerLogging:
get_crawler(MySpider)
assert get_scrapy_root_handler() is None
def test_spider_custom_settings_log_levels(self) -> None:
class MySpider(scrapy.Spider):
name = "spider"
custom_settings = {"LOG_LEVELS": {"httpx": "DEBUG"}}
try:
configure_logging()
assert logging.getLogger("httpx").level == logging.WARNING
get_crawler(MySpider)
assert logging.getLogger("httpx").level == logging.DEBUG
finally:
_uninstall_scrapy_root_handler()
logging.getLogger("httpx").setLevel(logging.NOTSET)
@coroutine_test
async def test_spider_custom_settings_log_level(self, tmp_path: Path) -> None:
log_file = Path(tmp_path, "log.txt")

View File

@ -17,6 +17,7 @@ from scrapy.utils.log import (
SpiderLoggerAdapter,
StreamLogger,
TopLevelFormatter,
configure_logging,
failure_to_exc_info,
logformatter_adapter,
)
@ -160,6 +161,29 @@ def test_spider_logger_adapter_process(
assert result_kwargs == expected_extra
class TestLogLevels:
@pytest.fixture(autouse=True)
def restore_levels(self) -> Generator[None]:
yield
for name in ("httpx", "boto3"):
logging.getLogger(name).setLevel(logging.NOTSET)
def test_defaults(self) -> None:
configure_logging(install_root_handler=False)
assert logging.getLogger("httpx").level == logging.WARNING
assert logging.getLogger("boto3").level == logging.NOTSET
def test_override(self) -> None:
configure_logging(
{"LOG_LEVELS": {"httpx": "DEBUG", "boto3": "ERROR"}},
install_root_handler=False,
)
assert logging.getLogger("httpx").level == logging.DEBUG
assert logging.getLogger("boto3").level == logging.ERROR
class TestLogging:
@pytest.fixture
def log_stream(self) -> StringIO: