Skip to content

Commit 4e80f17

Browse files
authored
fix(logging): configure each stdlib logger once and leave its name and preset level alone (#358)
1 parent 98b283f commit 4e80f17

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

‎lite_bootstrap/instruments/logging_factory.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,16 @@ def __init__(
126126
config: "_MemoryLoggerFactoryConfig",
127127
**kwargs: typing.Any, # noqa: ANN401
128128
) -> None:
129+
kwargs["ignore_frame_names"] = [*(kwargs.get("ignore_frame_names") or ()), __name__]
129130
super().__init__(*args, **kwargs)
130131
self.config = config
131-
self._created_handlers: list[tuple[logging.Logger, logging.handlers.MemoryHandler]] = []
132+
self._created_handlers: dict[logging.Logger, logging.handlers.MemoryHandler] = {}
133+
self._leveled_loggers: set[logging.Logger] = set()
132134

133135
def __call__(self, *args: typing.Any) -> logging.Logger: # noqa: ANN401
134136
logger: typing.Final = super().__call__(*args)
137+
if logger in self._created_handlers:
138+
return logger
135139
stream_handler: typing.Final = logging.StreamHandler(stream=self.config.log_stream)
136140
stream_handler.setFormatter(_RenderedLineFormatter())
137141
handler: typing.Final = logging.handlers.MemoryHandler(
@@ -140,20 +144,25 @@ def __call__(self, *args: typing.Any) -> logging.Logger: # noqa: ANN401
140144
target=stream_handler,
141145
)
142146
logger.addHandler(handler)
143-
logger.setLevel(self.config.logging_log_level)
147+
if logger.level == logging.NOTSET:
148+
logger.setLevel(self.config.logging_log_level)
149+
self._leveled_loggers.add(logger)
144150
logger.propagate = False
145-
self._created_handlers.append((logger, handler))
151+
self._created_handlers[logger] = handler
146152
return logger
147153

148154
def close_handlers(self) -> None:
149-
for created_logger, handler in self._created_handlers:
155+
for created_logger, handler in self._created_handlers.items():
150156
created_logger.removeHandler(handler)
151157
created_logger.propagate = True
158+
if created_logger in self._leveled_loggers:
159+
created_logger.setLevel(logging.NOTSET)
152160
target = handler.target
153161
handler.close()
154162
if target is not None:
155163
target.close()
156164
self._created_handlers.clear()
165+
self._leveled_loggers.clear()
157166

158167

159168
__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS)

‎tests/instruments/test_logging_instrument.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,63 @@ def test_memory_logger_factory_error() -> None:
129129
assert error_message in test_stream.getvalue()
130130

131131

132+
def _unbuffered_factory(stream: StringIO) -> MemoryLoggerFactory:
133+
return MemoryLoggerFactory(
134+
config=_MemoryLoggerFactoryConfig(
135+
logging_buffer_capacity=0,
136+
logging_flush_level=logging.ERROR,
137+
logging_log_level=logging.INFO,
138+
log_stream=stream,
139+
),
140+
)
141+
142+
143+
def test_memory_logger_factory_configures_each_logger_once() -> None:
144+
test_stream = StringIO()
145+
logger_factory = _unbuffered_factory(test_stream)
146+
try:
147+
first = logger_factory("configured_once")
148+
second = logger_factory("configured_once")
149+
second.info("one line")
150+
151+
assert first is second
152+
assert len(first.handlers) == 1
153+
assert test_stream.getvalue().splitlines() == ["one line"]
154+
finally:
155+
logger_factory.close_handlers()
156+
157+
assert logging.getLogger("configured_once").handlers == []
158+
159+
160+
def test_memory_logger_factory_names_unnamed_logger_after_caller() -> None:
161+
logger_factory = _unbuffered_factory(StringIO())
162+
try:
163+
assert logger_factory().name == __name__
164+
finally:
165+
logger_factory.close_handlers()
166+
167+
168+
def test_memory_logger_factory_keeps_a_level_set_beforehand() -> None:
169+
preset_logger = logging.getLogger("preset_level")
170+
preset_logger.setLevel(logging.DEBUG)
171+
logger_factory = _unbuffered_factory(StringIO())
172+
try:
173+
assert logger_factory("preset_level").level == logging.DEBUG
174+
assert logger_factory("unset_level").level == logging.INFO
175+
finally:
176+
logger_factory.close_handlers()
177+
preset_logger.setLevel(logging.NOTSET)
178+
logging.getLogger("unset_level").setLevel(logging.NOTSET)
179+
180+
181+
def test_memory_logger_factory_close_handlers_restores_the_level_it_set() -> None:
182+
logger_factory = _unbuffered_factory(StringIO())
183+
logger_factory("restored_level")
184+
logger_factory.close_handlers()
185+
186+
assert logging.getLogger("restored_level").level == logging.NOTSET
187+
188+
132189
def test_logging_instrument_lifecycle_replay(logging_mock: LoggingMock) -> None:
133190
instrument = LoggingInstrument(
134191
bootstrap_config=LoggingConfig(

0 commit comments

Comments
 (0)