diff --git a/README.md b/README.md index b61f73d..f8c30d6 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ and emit; their records flow into the handlers `log_setup` wired. ## Install ``` -log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.4.1 +log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.4.2 ``` No dependencies — stdlib only. -Drop the `@v0.4.1` suffix from the line above to install the latest unpinned. +Drop the `@v0.4.2` suffix from the line above to install the latest unpinned. ## Quick start diff --git a/pyproject.toml b/pyproject.toml index ac9062e..54431a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "log_setup" -version = "0.4.1" +version = "0.4.2" description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format" requires-python = ">=3.10" dependencies = [] diff --git a/src/log_setup/__init__.py b/src/log_setup/__init__.py index 1cdf4c8..7324eec 100644 --- a/src/log_setup/__init__.py +++ b/src/log_setup/__init__.py @@ -19,4 +19,4 @@ from .setup import setup_logging __all__ = ["setup_logging"] -__version__ = "0.4.1" +__version__ = "0.4.2" diff --git a/src/log_setup/rotation.py b/src/log_setup/rotation.py index a5e8108..467d9a9 100644 --- a/src/log_setup/rotation.py +++ b/src/log_setup/rotation.py @@ -62,6 +62,30 @@ def make_namer(log_dir: str, compress: bool) -> Callable[[str], str]: return namer +def make_size_namer( + stem: str, log_dir: str, clock=time.localtime, +) -> Callable[[str], str]: + """namer for tiered SIZE mode: a unique timestamped dest per roll, plain (no .gz) + + stdlib RotatingFileHandler names rolls `.1`, `.2`, ... and shifts them — + a scheme that breaks once files are redirected into log_dir (the shift can't find + them, so every roll reuses slot 1). tiered retention wants unique per-roll names it + can rank + tier like the daily/on_start paths, so ignore the handler's `.N` suffix + entirely and mint `..log`, disambiguating a same-second collision + (against both the .log and .log.gz forms) with a counter. always plain — retier + decides compression. + """ + def namer(default_name: str) -> str: + stamp = time.strftime("%Y-%m-%d_%H-%M-%S", clock()) + dest = os.path.join(log_dir, f"{stem}.{stamp}.log") + counter = 1 + while os.path.exists(dest) or os.path.exists(dest + ".gz"): + dest = os.path.join(log_dir, f"{stem}.{stamp}.{counter}.log") + counter += 1 + return dest + return namer + + def make_rotator( compress: bool, log_dir: Optional[str] = None, prune_stem: Optional[str] = None, backup_count: int = 0, @@ -231,6 +255,7 @@ def attach_rolling( handler, log_dir: str, compress: bool, prune_stem: Optional[str] = None, backup_count: int = 0, keep_uncompressed: Optional[int] = None, keep_compressed: Optional[int] = None, + size_tiered: bool = False, ) -> Tuple[Callable, Callable]: """wire the custom namer + rotator onto a rotating handler; return them @@ -238,8 +263,16 @@ def attach_rolling( (the handler's own retention can't see the redirected rolled files). pass `keep_uncompressed`/`keep_compressed` instead to use tiered retention (newest plain, next gzipped, rest deleted) — see make_rotator. + + `size_tiered` uses a timestamped per-roll namer (make_size_namer) instead of the + default one, for a tiered RotatingFileHandler (size mode): stdlib's `.1/.2` numbered + shift can't manage files redirected into log_dir, so each roll gets a unique dated + name that retier ranks/tiers like the daily/on_start paths. """ - namer = make_namer(log_dir, compress) + if size_tiered: + namer = make_size_namer(os.path.basename(prune_stem or ""), log_dir) + else: + namer = make_namer(log_dir, compress) rotator = make_rotator( compress, log_dir, prune_stem, backup_count, keep_uncompressed, keep_compressed, ) diff --git a/src/log_setup/setup.py b/src/log_setup/setup.py index 6c01aa0..65f461f 100644 --- a/src/log_setup/setup.py +++ b/src/log_setup/setup.py @@ -128,12 +128,18 @@ def _file_handler( """build the configured file handler with custom rolling into log_dir""" tiered = keep_uncompressed is not None or keep_compressed is not None if rotate == "size": + # stdlib doRollover is a no-op when backupCount == 0, and its numbered .1/.2 shift + # can't manage files redirected into log_dir. in tiered size mode force a nonzero + # backupCount so the roll always fires (retier bounds retention, not backupCount) + # and use the timestamped size-namer (size_tiered) instead of the .N shift. + size_backup = backup_count if not tiered else max(backup_count, 1) handler = logging.handlers.RotatingFileHandler( - live_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8", + live_path, maxBytes=max_bytes, backupCount=size_backup, encoding="utf-8", ) attach_rolling( handler, log_dir, compress, prune_stem=name, backup_count=backup_count, keep_uncompressed=keep_uncompressed, keep_compressed=keep_compressed, + size_tiered=tiered, ) elif rotate == "daily": handler = logging.handlers.TimedRotatingFileHandler(