diff --git a/src/log_setup/rotation.py b/src/log_setup/rotation.py index 904784a..9f0088e 100644 --- a/src/log_setup/rotation.py +++ b/src/log_setup/rotation.py @@ -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: diff --git a/src/log_setup/setup.py b/src/log_setup/setup.py index f65cbfa..7defbfd 100644 --- a/src/log_setup/setup.py +++ b/src/log_setup/setup.py @@ -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,