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 <dev@disqualifier.me>
This commit is contained in:
2026-07-06 00:14:07 -04:00
parent e12a978a2b
commit a6edb965d0
+17 -1
View File
@@ -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()