|
10 | 10 | import logging |
11 | 11 | import logging.handlers |
12 | 12 | import queue |
13 | | -import sys |
14 | 13 | from asyncio import iscoroutinefunction |
15 | 14 | from collections.abc import Callable, Iterator |
16 | 15 | from contextlib import contextmanager |
@@ -155,19 +154,25 @@ def format(self, record) -> str: |
155 | 154 | # log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{DATA:log_source} \| (log_uid=%{WORD:log_uid} \| )?log_msg=%{GREEDYDATA:log_msg} |
156 | 155 |
|
157 | 156 |
|
158 | | -def _setup_format_string( |
| 157 | +def _setup_logging_formatter( |
159 | 158 | *, |
160 | 159 | tracing_settings: TracingSettings | None, |
161 | 160 | log_format_local_dev_enabled: bool, |
162 | | -) -> str: |
| 161 | +) -> logging.Formatter: |
163 | 162 | if log_format_local_dev_enabled: |
164 | | - return ( |
| 163 | + fmt = ( |
165 | 164 | _LOCAL_TRACING_FORMATTING |
166 | 165 | if tracing_settings is not None |
167 | 166 | else _LOCAL_FORMATTING |
168 | 167 | ) |
| 168 | + else: |
| 169 | + fmt = ( |
| 170 | + _TRACING_FORMATTING if tracing_settings is not None else _DEFAULT_FORMATTING |
| 171 | + ) |
169 | 172 |
|
170 | | - return _TRACING_FORMATTING if tracing_settings is not None else _DEFAULT_FORMATTING |
| 173 | + return CustomFormatter( |
| 174 | + fmt, log_format_local_dev_enabled=log_format_local_dev_enabled |
| 175 | + ) |
171 | 176 |
|
172 | 177 |
|
173 | 178 | def _get_all_loggers() -> list[logging.Logger]: |
@@ -260,25 +265,69 @@ def setup_loggers( |
260 | 265 | _dampen_noisy_loggers(noisy_loggers) |
261 | 266 | if tracing_settings is not None: |
262 | 267 | setup_log_tracing(tracing_settings=tracing_settings) |
263 | | - fmt = _setup_format_string( |
| 268 | + formatter = _setup_logging_formatter( |
264 | 269 | tracing_settings=tracing_settings, |
265 | 270 | log_format_local_dev_enabled=log_format_local_dev_enabled, |
266 | 271 | ) |
267 | 272 |
|
268 | 273 | # Create a properly formatted handler for the root logger |
269 | | - root_handler = logging.StreamHandler() |
270 | | - root_handler.setFormatter( |
271 | | - CustomFormatter(fmt, log_format_local_dev_enabled=log_format_local_dev_enabled) |
| 274 | + stream_handler = logging.StreamHandler() |
| 275 | + stream_handler.setFormatter(formatter) |
| 276 | + |
| 277 | + _store_logger_state(_get_all_loggers()) |
| 278 | + _clean_all_handlers() |
| 279 | + _set_root_handler(stream_handler) |
| 280 | + |
| 281 | + if logger_filter_mapping: |
| 282 | + _apply_logger_filters(logger_filter_mapping) |
| 283 | + |
| 284 | + |
| 285 | +@contextmanager |
| 286 | +def _queued_logging_handler( |
| 287 | + log_formatter: logging.Formatter, |
| 288 | +) -> Iterator[logging.Handler]: |
| 289 | + log_queue: queue.Queue[logging.LogRecord] = queue.Queue() |
| 290 | + # Create handler with proper formatting |
| 291 | + handler = logging.StreamHandler() |
| 292 | + handler.setFormatter(log_formatter) |
| 293 | + |
| 294 | + # Create and start the queue listener |
| 295 | + listener = logging.handlers.QueueListener( |
| 296 | + log_queue, handler, respect_handler_level=True |
272 | 297 | ) |
| 298 | + listener.start() |
| 299 | + |
| 300 | + queue_handler = logging.handlers.QueueHandler(log_queue) |
| 301 | + |
| 302 | + yield queue_handler |
| 303 | + |
| 304 | + # cleanup |
| 305 | + with log_context( |
| 306 | + _logger, |
| 307 | + level=logging.DEBUG, |
| 308 | + msg="Shutdown async logging listener", |
| 309 | + ): |
| 310 | + listener.stop() |
273 | 311 |
|
| 312 | + |
| 313 | +def _clean_all_handlers() -> None: |
| 314 | + """ |
| 315 | + Cleans all handlers from all loggers. |
| 316 | + This is useful for resetting the logging configuration. |
| 317 | + """ |
| 318 | + root_logger = logging.getLogger() |
274 | 319 | all_loggers = _get_all_loggers() |
| 320 | + for logger in all_loggers: |
| 321 | + if logger is root_logger: |
| 322 | + continue |
| 323 | + logger.handlers.clear() |
| 324 | + logger.propagate = True # Ensure propagation is enabled |
275 | 325 |
|
276 | | - # Apply comprehensive logging setup |
277 | | - # Note: We don't store the original state here since this is a permanent setup |
278 | | - _apply_comprehensive_logging_setup(all_loggers, root_handler) |
279 | 326 |
|
280 | | - # Apply filters |
281 | | - _apply_logger_filters(logger_filter_mapping) |
| 327 | +def _set_root_handler(handler: logging.Handler) -> None: |
| 328 | + root_logger = logging.getLogger() |
| 329 | + root_logger.handlers.clear() # Clear existing handlers |
| 330 | + root_logger.addHandler(handler) # Add the new handler |
282 | 331 |
|
283 | 332 |
|
284 | 333 | @contextmanager |
@@ -338,57 +387,24 @@ def async_loggers( |
338 | 387 |
|
339 | 388 | if tracing_settings is not None: |
340 | 389 | setup_log_tracing(tracing_settings=tracing_settings) |
341 | | - fmt = _setup_format_string( |
| 390 | + formatter = _setup_logging_formatter( |
342 | 391 | tracing_settings=tracing_settings, |
343 | 392 | log_format_local_dev_enabled=log_format_local_dev_enabled, |
344 | 393 | ) |
345 | 394 |
|
346 | | - # Set up async logging infrastructure |
347 | | - log_queue: queue.Queue[logging.LogRecord] = queue.Queue() |
348 | | - # Create handler with proper formatting |
349 | | - handler = logging.StreamHandler() |
350 | | - handler.setFormatter( |
351 | | - CustomFormatter(fmt, log_format_local_dev_enabled=log_format_local_dev_enabled) |
352 | | - ) |
353 | | - |
354 | | - # Create and start the queue listener |
355 | | - listener = logging.handlers.QueueListener( |
356 | | - log_queue, handler, respect_handler_level=True |
357 | | - ) |
358 | | - listener.start() |
359 | | - |
360 | | - # Create queue handler for loggers |
361 | | - queue_handler = logging.handlers.QueueHandler(log_queue) |
362 | | - |
363 | | - # Apply comprehensive logging setup and store original state for restoration |
364 | | - all_loggers = _get_all_loggers() |
365 | | - original_logger_state = _apply_comprehensive_logging_setup( |
366 | | - all_loggers, queue_handler |
367 | | - ) |
| 395 | + with ( |
| 396 | + _queued_logging_handler(formatter) as queue_handler, |
| 397 | + _stored_logger_states(_get_all_loggers()), |
| 398 | + ): |
| 399 | + _clean_all_handlers() |
| 400 | + _set_root_handler(queue_handler) |
368 | 401 |
|
369 | | - try: |
370 | 402 | if logger_filter_mapping: |
371 | 403 | _apply_logger_filters(logger_filter_mapping) |
372 | 404 |
|
373 | 405 | with log_context(_logger, logging.INFO, "Asynchronous logging"): |
374 | 406 | yield |
375 | 407 |
|
376 | | - finally: |
377 | | - try: |
378 | | - _restore_logger_state(original_logger_state) |
379 | | - |
380 | | - # Stop the queue listener |
381 | | - with log_context( |
382 | | - _logger, |
383 | | - level=logging.DEBUG, |
384 | | - msg="Shutdown async logging listener", |
385 | | - ): |
386 | | - listener.stop() |
387 | | - |
388 | | - except Exception as exc: # pylint: disable=broad-except |
389 | | - sys.stderr.write(f"Error during async logging cleanup: {exc}\n") |
390 | | - sys.stderr.flush() |
391 | | - |
392 | 408 |
|
393 | 409 | class LogExceptionsKwargsDict(TypedDict, total=True): |
394 | 410 | logger: logging.Logger |
@@ -642,10 +658,37 @@ class _LoggerState: |
642 | 658 | propagate: bool |
643 | 659 |
|
644 | 660 |
|
| 661 | +@contextmanager |
| 662 | +def _stored_logger_states( |
| 663 | + loggers: list[logging.Logger], |
| 664 | +) -> Iterator[list[_LoggerState]]: |
| 665 | + """ |
| 666 | + Context manager to store and restore the state of loggers. |
| 667 | + It captures the current handlers and propagation state of each logger. |
| 668 | + """ |
| 669 | + original_state = _store_logger_state(loggers) |
| 670 | + |
| 671 | + try: |
| 672 | + # log which loggers states were stored |
| 673 | + _logger.info( |
| 674 | + "Stored logger states: %s. TIP: these loggers configuration will be restored later.", |
| 675 | + json_dumps( |
| 676 | + [ |
| 677 | + f"{state.logger.name}(handlers={len(state.handlers)}, propagate={state.propagate})" |
| 678 | + for state in original_state |
| 679 | + ] |
| 680 | + ), |
| 681 | + ) |
| 682 | + yield original_state |
| 683 | + finally: |
| 684 | + _restore_logger_state(original_state) |
| 685 | + |
| 686 | + |
645 | 687 | def _store_logger_state(loggers: list[logging.Logger]) -> list[_LoggerState]: |
646 | 688 | return [ |
647 | 689 | _LoggerState(logger, logger.handlers.copy(), logger.propagate) |
648 | 690 | for logger in loggers |
| 691 | + if logger.handlers or not logger.propagate |
649 | 692 | ] |
650 | 693 |
|
651 | 694 |
|
|
0 commit comments