fix: daily roll spares cwd, failed roll keeps logging, queue formats at call

T1-2: TimedRotatingFileHandler(backupCount=N) on py3.10/3.11 runs the
stdlib's own retention over the live file's dir (cwd) and deleted unrelated
files. pass backupCount=0; make_rotator's prune already owns retention.

T1-3: an OSError from move/gzip/prune escaped into doRollover, rolloverAt
never advanced and every later record was dropped. the rotator now writes
one line to stderr and returns, and the stdlib reopens the live file.

T2-6: queue=True deferred %-formatting to the listener thread, so a mutable
arg logged its mutated value. prepare() merges msg+args on the calling
thread; exc_info stays for the listener's formatter.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
dsql committed 2026-10-16 03:57:59 -04:00
1 parent d3f109db15
commit 76d3efad96
2 files changed
+21 -2

No files matched your search

+12
View File
@@ -16,6 +16,7 @@ import gzip
import os import os
import re import re
import shutil import shutil
import sys
import time import time
from typing import Callable, Optional, Tuple from typing import Callable, Optional, Tuple
@@ -150,10 +151,21 @@ def make_rotator(
tiered mode (when `keep_uncompressed`/`keep_compressed` are given): land the rolled tiered mode (when `keep_uncompressed`/`keep_compressed` are given): land the rolled
file PLAIN and re-tier `log_dir` - newest `keep_uncompressed` stay uncompressed, next file PLAIN and re-tier `log_dir` - newest `keep_uncompressed` stay uncompressed, next
`keep_compressed` gzipped, rest deleted. `compress`/`backup_count` are ignored. `keep_compressed` gzipped, rest deleted. `compress`/`backup_count` are ignored.
a failed roll (OSError from move/gzip/prune, e.g. `log_dir` deleted) is reported on
stderr and swallowed: raising into the stdlib doRollover would leave the handler
retrying the roll and dropping every later record. the stdlib then reopens the live
file and logging continues there. stderr, not logging - this runs inside emit.
""" """
tiered = keep_uncompressed is not None or keep_compressed is not None tiered = keep_uncompressed is not None or keep_compressed is not None
def rotator(source: str, dest: str) -> None: def rotator(source: str, dest: str) -> None:
try:
_roll(source, dest)
except OSError as error:
sys.stderr.write(f"log_setup: rotation of {source!r} failed, still logging to it: {error}\n")
def _roll(source: str, dest: str) -> None:
if not os.path.exists(source): if not os.path.exists(source):
return return
if tiered: if tiered:
+9 -2
View File
@@ -38,10 +38,15 @@ class _PreservingQueueHandler(logging.handlers.QueueHandler):
listener's real formatter ever sees the record. that behavior exists to keep a 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 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 - in-process queue.Queue, so there is nothing to pickle and nothing to strip -
the listener's handler formats the untouched record exactly once. the listener's handler formats the record exactly once, exc_info intact.
the message IS merged with its args here, on the calling thread: deferring that to
the listener would log a mutable arg with whatever value it holds by then.
""" """
def prepare(self, record: logging.LogRecord) -> logging.LogRecord: def prepare(self, record: logging.LogRecord) -> logging.LogRecord:
record.msg = record.getMessage()
record.args = None
return record return record
@@ -168,8 +173,10 @@ def _file_handler(
tiered=tiered, rotate_mode=rotate, tiered=tiered, rotate_mode=rotate,
) )
elif rotate == "daily": elif rotate == "daily":
# backupCount=0: on py3.10/3.11 the stdlib's own retention scans the live file's
# dir (cwd) and deletes unrelated files; make_rotator's prune owns retention
handler = logging.handlers.TimedRotatingFileHandler( handler = logging.handlers.TimedRotatingFileHandler(
live_path, when="midnight", backupCount=backup_count, encoding="utf-8", live_path, when="midnight", backupCount=0, encoding="utf-8",
) )
attach_rolling( attach_rolling(
handler, log_dir, compress, prune_stem=history_stem, backup_count=backup_count, handler, log_dir, compress, prune_stem=history_stem, backup_count=backup_count,