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()