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:
1 parent
d3f109db15
commit
76d3efad96
2 files changed
+21
-2
No files matched your search
@@ -16,6 +16,7 @@ import gzip
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
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
|
||||
file PLAIN and re-tier `log_dir` - newest `keep_uncompressed` stay uncompressed, next
|
||||
`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
|
||||
|
||||
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):
|
||||
return
|
||||
if tiered:
|
||||
|
||||
@@ -38,10 +38,15 @@ class _PreservingQueueHandler(logging.handlers.QueueHandler):
|
||||
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.
|
||||
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:
|
||||
record.msg = record.getMessage()
|
||||
record.args = None
|
||||
return record
|
||||
|
||||
|
||||
@@ -168,8 +173,10 @@ def _file_handler(
|
||||
tiered=tiered, rotate_mode=rotate,
|
||||
)
|
||||
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(
|
||||
live_path, when="midnight", backupCount=backup_count, encoding="utf-8",
|
||||
live_path, when="midnight", backupCount=0, encoding="utf-8",
|
||||
)
|
||||
attach_rolling(
|
||||
handler, log_dir, compress, prune_stem=history_stem, backup_count=backup_count,
|
||||
|
||||
Reference in new issue
Block a user