Skip to content

Commit 2cc5602

Browse files
authored
fix(scrapy): make logging configuration idempotent (#954)
`initialize_logging` re-wrapped `scrapy.utils.log.configure_logging` on every call, stacking wrappers when it ran more than once. The monkey-patch is now installed at most once and reads the current handler and level from a module-level state dict, so repeated initialization stays correct without nesting wrappers. Part of splitting the larger Scrapy integration fix (`fix/scrapy-integration`) into reviewable pieces.
1 parent ee0b011 commit 2cc5602

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

src/apify/scrapy/_logging_config.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
_SUPPLEMENTAL_LOGGERS = ['filelock', 'hpack', 'httpcore', 'protego', 'twisted']
1414
_ALL_LOGGERS = _PRIMARY_LOGGERS + _SUPPLEMENTAL_LOGGERS
1515

16+
# Mutable state shared with the Scrapy monkey-patch below. `initialize_logging` refreshes
17+
# `level`/`handler` on each call; the patch (installed once) reads them so it always applies the
18+
# latest configuration rather than values captured the first time it ran.
19+
_state: dict[str, Any] = {'level': 'INFO', 'handler': None, 'patched': False}
20+
1621

1722
def _configure_logger(name: str | None, logging_level: str, handler: logging.Handler) -> None:
1823
"""Clear and reconfigure the logger."""
@@ -23,26 +28,39 @@ def _configure_logger(name: str | None, logging_level: str, handler: logging.Han
2328
logger.propagate = False
2429

2530

31+
def _configure_all_loggers() -> None:
32+
"""Apply the Apify handler and level to the root logger and all defined loggers."""
33+
handler = _state['handler']
34+
if handler is None:
35+
return
36+
for logger_name in [None, *_ALL_LOGGERS]:
37+
_configure_logger(logger_name, _state['level'], handler)
38+
39+
2640
def initialize_logging() -> None:
2741
"""Configure logging for Apify Actors and adjust Scrapy's logging settings."""
2842
# Retrieve Scrapy project settings and determine the logging level.
2943
settings = get_project_settings()
30-
logging_level = settings.get('LOG_LEVEL', 'INFO') # Default to INFO.
44+
_state['level'] = settings.get('LOG_LEVEL', 'INFO') # Default to INFO.
3145

3246
# Create a custom handler with the Apify log formatter.
3347
handler = logging.StreamHandler()
3448
handler.setFormatter(ActorLogFormatter(include_logger_name=True))
49+
_state['handler'] = handler
3550

3651
# Configure the root logger and all other defined loggers.
37-
for logger_name in [None, *_ALL_LOGGERS]:
38-
_configure_logger(logger_name, logging_level, handler)
52+
_configure_all_loggers()
53+
54+
# Monkey-patch Scrapy's logging to re-apply our settings whenever it reconfigures logging.
55+
# Install the wrapper at most once, otherwise repeated calls would nest wrappers.
56+
if _state['patched']:
57+
return
3958

40-
# Monkey-patch Scrapy's logging configuration to re-apply our settings.
4159
original_configure_logging = scrapy_logging.configure_logging
4260

4361
def new_configure_logging(*args: Any, **kwargs: Any) -> None:
4462
original_configure_logging(*args, **kwargs)
45-
for logger_name in [None, *_ALL_LOGGERS]:
46-
_configure_logger(logger_name, logging_level, handler)
63+
_configure_all_loggers()
4764

4865
scrapy_logging.configure_logging = new_configure_logging # ty: ignore[invalid-assignment]
66+
_state['patched'] = True

0 commit comments

Comments
 (0)