Skip to content

Commit 914346a

Browse files
committed
2. fix morebuiltins.logs.LogHelper.bind_handler to avoid adding duplicate queue handlers.
1 parent c0fc347 commit 914346a

File tree

2 files changed

+59
-12
lines changed

2 files changed

+59
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### 1.3.3 (2025-09-01)
44
1. add `morebuiltins.logs.LogHelper.handle_crash` to log uncaught exceptions.
5+
2. fix `morebuiltins.logs.LogHelper.bind_handler` to avoid adding duplicate queue handlers.
56

67
### 1.3.2 (2025-08-02)
78
1. fix typing-hint for `morebuiltins.funcs.threads` decorator, now it returns `Callable[..., Future]`.

morebuiltins/logs.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ class LogHelper:
4242
4343
Examples::
4444
45+
# 1. Bind a StreamHandler to the "mylogger" logger, output to sys.stdout
4546
import logging
46-
from morebuiltins.log import LogHelper
47+
from morebuiltins.logs import LogHelper
4748
4849
LogHelper.shorten_level()
4950
logger = LogHelper.bind_handler(name="mylogger", filename=sys.stdout, maxBytes=100 * 1024**2, backupCount=7)
@@ -52,6 +53,23 @@ class LogHelper:
5253
assert logger is logger2
5354
logger.info("This is an info message")
5455
logger.fatal("This is a critical message")
56+
57+
# 2. Bind file and stderr in the same logger
58+
import sys
59+
import logging
60+
from morebuiltins.logs import LogHelper
61+
LogHelper.shorten_level()
62+
logger = LogHelper.bind_handler(name="mylogger", filename="mylog.log", maxBytes=100 * 1024**2, backupCount=7)
63+
logger = LogHelper.bind_handler(name="mylogger", filename=sys.stderr)
64+
logger.info("This is an info message")
65+
66+
# 3. Use queue=True to make logging non-blocking, both file and stderr
67+
import sys
68+
from morebuiltins.logs import LogHelper
69+
LogHelper.shorten_level()
70+
logger = LogHelper.bind_handler(name="mylogger", filename="mylog.log", maxBytes=100 * 1024**2, backupCount=7, queue=True)
71+
logger = LogHelper.bind_handler(name="mylogger", filename=sys.stderr, queue=True)
72+
logger.info("This is an info message")
5573
"""
5674

5775
DEFAULT_FORMAT = (
@@ -192,15 +210,28 @@ def bind_handler(
192210
formatter = logging.Formatter(cls.DEFAULT_FORMAT)
193211
handler.setFormatter(formatter)
194212
# Add the handler to the logger
195-
logger.addHandler(handler)
196213
if queue:
197214
if queue is True:
198215
queue = Queue()
199216
elif not isinstance(queue, (Queue, ProcessQueue)):
200217
raise TypeError("queue must be a Queue, ProcessQueue, True, or False")
201-
async_listener = AsyncQueueListener(logger, queue=queue)
202-
async_listener.start()
203-
atexit.register(async_listener.stop)
218+
if (
219+
len(logger.handlers) == 1
220+
and isinstance(logger.handlers[0], QueueHandler)
221+
and isinstance(logger.handlers[0].listener, AsyncQueueListener)
222+
):
223+
# already a QueueHandler
224+
async_listener: AsyncQueueListener = logger.handlers[0].listener
225+
async_listener.bind_new_handler(handler)
226+
return logger
227+
else:
228+
logger.addHandler(handler)
229+
async_listener = AsyncQueueListener(logger, queue=queue)
230+
async_listener.start()
231+
atexit.register(async_listener.stop)
232+
else:
233+
if handler not in logger.handlers:
234+
logger.addHandler(handler)
204235
return logger
205236

206237
@classmethod
@@ -671,10 +702,11 @@ def __init__(
671702
respect_handler_level=True,
672703
):
673704
self.logger = logger
705+
self.queue_id = id(queue)
674706
self.queue = queue or Queue()
675-
# Store original handlers
707+
# Store original handlers to restore later
676708
self.original_handlers = list(logger.handlers)
677-
# Get handlers that might block
709+
# Get handlers that might block, send to parent class
678710
self.blocking_handlers = [
679711
h for h in self.original_handlers if not isinstance(h, QueueHandler)
680712
]
@@ -684,14 +716,24 @@ def __init__(
684716
*self.blocking_handlers,
685717
respect_handler_level=respect_handler_level,
686718
)
719+
self._started = self._stopped = False
720+
721+
def bind_new_handler(self, handler: logging.Handler):
722+
if isinstance(handler, QueueHandler):
723+
raise TypeError("handler cannot be a QueueHandler")
724+
self.original_handlers.append(handler)
725+
self.blocking_handlers.append(handler)
726+
self.handlers = tuple(self.blocking_handlers)
687727

688728
def _switch_to_queue_handler(self):
689729
"""Switch handlers in a blocking context"""
690730
# Remove original handlers
691731
for handler in self.original_handlers:
692732
self.logger.removeHandler(handler)
693733
# Add queue handler
694-
self.logger.addHandler(QueueHandler(self.queue))
734+
queue_handler = QueueHandler(self.queue)
735+
queue_handler.listener = self
736+
self.logger.addHandler(queue_handler)
695737

696738
def _restore_original_handlers(self):
697739
"""Restore original handlers in a blocking context"""
@@ -702,12 +744,16 @@ def _restore_original_handlers(self):
702744
self.logger.addHandler(handler)
703745

704746
def stop(self):
705-
super().stop()
706-
self._restore_original_handlers()
747+
if self._started and not self._stopped:
748+
self._stopped = True
749+
super().stop()
750+
self._restore_original_handlers()
707751

708752
def start(self):
709-
self._switch_to_queue_handler()
710-
super().start()
753+
if not self._started:
754+
self._started = True
755+
self._switch_to_queue_handler()
756+
super().start()
711757

712758
async def __aenter__(self):
713759
await to_thread(self.start)

0 commit comments

Comments
 (0)