From a6edb965d087f1ee08d8699e9796b8925251aeb6 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:14:07 -0400 Subject: [PATCH] fix: queue=True no longer strips exc_info before JsonLinesFormatter sees it QueueHandler.prepare() formatted the record with its own default formatter and nulled exc_info/exc_text/stack_info before the listener's real formatter ran, so queue=True + output="json" produced a JSON line with no exc_info key and the traceback folded into message instead. _PreservingQueueHandler skips that premature format-and-strip (safe here - the queue is in-process only, nothing needs to stay picklable) so the listener's handler formats the untouched record exactly once, matching the non-queued path. Signed-off-by: disqualifier --- src/log_setup/setup.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/log_setup/setup.py b/src/log_setup/setup.py index 356cb89..bafefc9 100644 --- a/src/log_setup/setup.py +++ b/src/log_setup/setup.py @@ -29,6 +29,22 @@ _listener = None _atexit_registered = False +class _PreservingQueueHandler(logging.handlers.QueueHandler): + """QueueHandler that hands the record to the listener untouched + + stdlib's QueueHandler.prepare() calls self.format(record) with the queue + handler's OWN (default) formatter, then nulls exc_info/exc_text/stack_info - + losing structured fields (e.g. JsonLinesFormatter's exc_info key) before the + listener's real formatter ever sees the record. that behavior exists to keep a + record picklable across a multiprocessing.Queue; this lib only ever uses an + in-process queue.Queue, so there is nothing to pickle and nothing to strip - + the listener's handler formats the untouched record exactly once. + """ + + def prepare(self, record: logging.LogRecord) -> logging.LogRecord: + return record + + def _exc_text() -> str: """render sys.exc_info() as text, for capturing a traceback into a buffered warning""" return "".join(traceback.format_exception(*sys.exc_info())).strip() @@ -284,7 +300,7 @@ def setup_logging( if queue: record_queue: "_queue.Queue" = _make_queue() - qh = _tag(logging.handlers.QueueHandler(record_queue)) + qh = _tag(_PreservingQueueHandler(record_queue)) root.addHandler(qh) _listener = logging.handlers.QueueListener(record_queue, *handlers, respect_handler_level=True) _listener.start()