fix: rotate-mode-aware zero-retention delete in make_rotator (logsetup-1)
v0.5.1's rotate="size" fix added an unconditional os.remove(dest) at backup_count<=0 in make_rotator to bound the live file, but the rotator is shared with rotate="daily" and had no mode awareness - every midnight roll under daily with backup_count=0 was deleting the just-rolled log instead of leaving it unpruned. make_rotator/attach_rolling now take rotate_mode and gate the delete-on-land branch to "size" only, matching the documented contract that only size always bounds the live file. Bump 0.6.1 -> 0.6.2. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -13,12 +13,12 @@ and emit; their records flow into the handlers `log_setup` wired.
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.6.1
|
log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.6.2
|
||||||
```
|
```
|
||||||
|
|
||||||
No dependencies — stdlib only.
|
No dependencies — stdlib only.
|
||||||
|
|
||||||
Drop the `@v0.6.1` suffix from the line above to install the latest unpinned.
|
Drop the `@v0.6.2` suffix from the line above to install the latest unpinned.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -248,6 +248,12 @@ setup_logging(name="run", queue=True)
|
|||||||
documented (keeps that many rolled files). This does not change `"daily"`/`"on_start"`,
|
documented (keeps that many rolled files). This does not change `"daily"`/`"on_start"`,
|
||||||
where `backup_count=0` still means "roll, but don't prune the rolled files" (unbounded
|
where `backup_count=0` still means "roll, but don't prune the rolled files" (unbounded
|
||||||
`log_dir` growth) — that is a separate, pre-existing knob, not this fix's scope.
|
`log_dir` growth) — that is a separate, pre-existing knob, not this fix's scope.
|
||||||
|
- **`"daily"` regression fixed (v0.6.2).** v0.5.1's `rotate="size"` fix above shared its
|
||||||
|
rotator with `"daily"`, so a `rotate="daily", backup_count=0` roll was incorrectly
|
||||||
|
deleted at every midnight rollover instead of just landing unpruned. The rotator is now
|
||||||
|
rotate-mode aware: the zero-retention delete only ever fires for `"size"`, matching the
|
||||||
|
contract in the bullet above — `"daily"`/`"on_start"` with `backup_count=0` were always
|
||||||
|
meant to roll without pruning and now do again.
|
||||||
- **Gzip writes are crash-safe (v0.5.1+).** `_gzip_file` now writes to a `.tmp` sibling and
|
- **Gzip writes are crash-safe (v0.5.1+).** `_gzip_file` now writes to a `.tmp` sibling and
|
||||||
atomically `os.replace`s it onto the final `.gz` path, so a crash/OOM/power-loss mid-write
|
atomically `os.replace`s it onto the final `.gz` path, so a crash/OOM/power-loss mid-write
|
||||||
can never leave a truncated `.gz` at the path retention logic trusts. Tiered retention's
|
can never leave a truncated `.gz` at the path retention logic trusts. Tiered retention's
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "log_setup"
|
name = "log_setup"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format"
|
description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ from .setup import setup_logging
|
|||||||
|
|
||||||
__all__ = ["setup_logging"]
|
__all__ = ["setup_logging"]
|
||||||
|
|
||||||
__version__ = "0.6.1"
|
__version__ = "0.6.2"
|
||||||
|
|||||||
+18
-10
@@ -143,16 +143,20 @@ def make_rotator(
|
|||||||
compress: bool, log_dir: Optional[str] = None,
|
compress: bool, log_dir: Optional[str] = None,
|
||||||
prune_stem: Optional[str] = None, backup_count: int = 0,
|
prune_stem: Optional[str] = None, backup_count: int = 0,
|
||||||
keep_uncompressed: Optional[int] = None, keep_compressed: Optional[int] = None,
|
keep_uncompressed: Optional[int] = None, keep_compressed: Optional[int] = None,
|
||||||
|
rotate_mode: Optional[str] = None,
|
||||||
) -> Callable[[str, str], None]:
|
) -> Callable[[str, str], None]:
|
||||||
"""rotator: move (or gzip) the source live file to the destination rolled path
|
"""rotator: move (or gzip) the source live file to the destination rolled path
|
||||||
|
|
||||||
legacy mode (default): gzip on roll when `compress`, then prune `log_dir` to
|
legacy mode (default): gzip on roll when `compress`, then prune `log_dir` to
|
||||||
`backup_count` newest rolled files - the stdlib handler's own retention only scans
|
`backup_count` newest rolled files - the stdlib handler's own retention only scans
|
||||||
the live file's directory, so it never sees files redirected into `log_dir`; pruning
|
the live file's directory, so it never sees files redirected into `log_dir`; pruning
|
||||||
here is what bounds retention for daily/size. FOOTGUN: `backup_count <= 0` means
|
here is what bounds retention for daily/size. FOOTGUN: for `rotate_mode="size"`,
|
||||||
"keep no rolled history", but `prune()` itself no-ops at `<= 0` (its own sentinel for
|
`backup_count <= 0` means "keep no rolled history", but `prune()` itself no-ops at
|
||||||
"don't touch history") - so a zero-retention roll is deleted by the rotator directly
|
`<= 0` (its own sentinel for "don't touch history") - so a zero-retention roll is
|
||||||
right after landing, rather than relying on prune to do it.
|
deleted by the rotator directly right after landing, rather than relying on prune to
|
||||||
|
do it. `rotate_mode` gates this delete-on-land branch to `"size"` only - `"daily"`
|
||||||
|
(and any other non-size mode) with `backup_count <= 0` still rolls without pruning,
|
||||||
|
matching the documented contract that only `size` always bounds the live file.
|
||||||
|
|
||||||
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
|
||||||
@@ -176,10 +180,11 @@ def make_rotator(
|
|||||||
else:
|
else:
|
||||||
_move(source, dest)
|
_move(source, dest)
|
||||||
if backup_count <= 0:
|
if backup_count <= 0:
|
||||||
try:
|
if rotate_mode == "size":
|
||||||
os.remove(dest)
|
try:
|
||||||
except OSError:
|
os.remove(dest)
|
||||||
pass
|
except OSError:
|
||||||
|
pass
|
||||||
elif log_dir is not None and prune_stem is not None:
|
elif log_dir is not None and prune_stem is not None:
|
||||||
prune(log_dir, prune_stem, backup_count)
|
prune(log_dir, prune_stem, backup_count)
|
||||||
return rotator
|
return rotator
|
||||||
@@ -359,7 +364,7 @@ def attach_rolling(
|
|||||||
handler, log_dir: str, compress: bool,
|
handler, log_dir: str, compress: bool,
|
||||||
prune_stem: Optional[str] = None, backup_count: int = 0,
|
prune_stem: Optional[str] = None, backup_count: int = 0,
|
||||||
keep_uncompressed: Optional[int] = None, keep_compressed: Optional[int] = None,
|
keep_uncompressed: Optional[int] = None, keep_compressed: Optional[int] = None,
|
||||||
tiered: bool = False,
|
tiered: bool = False, rotate_mode: Optional[str] = None,
|
||||||
) -> Tuple[Callable, Callable]:
|
) -> Tuple[Callable, Callable]:
|
||||||
"""wire the custom namer + rotator onto a rotating handler; return them
|
"""wire the custom namer + rotator onto a rotating handler; return them
|
||||||
|
|
||||||
@@ -372,13 +377,16 @@ def attach_rolling(
|
|||||||
pass `prune_stem`/`backup_count` so the rotator prunes `log_dir` after each roll
|
pass `prune_stem`/`backup_count` so the rotator prunes `log_dir` after each roll
|
||||||
(the handler's own retention can't see the redirected files). pass
|
(the handler's own retention can't see the redirected files). pass
|
||||||
`keep_uncompressed`/`keep_compressed` for tiered retention instead (see
|
`keep_uncompressed`/`keep_compressed` for tiered retention instead (see
|
||||||
make_rotator); `tiered=True` lands rolls plain (retier compresses).
|
make_rotator); `tiered=True` lands rolls plain (retier compresses). `rotate_mode`
|
||||||
|
("size"/"daily") is forwarded to `make_rotator` so the zero-retention delete-on-land
|
||||||
|
branch only ever fires for `"size"`.
|
||||||
"""
|
"""
|
||||||
namer = make_history_namer(
|
namer = make_history_namer(
|
||||||
os.path.basename(prune_stem or ""), log_dir, compress, plain=tiered,
|
os.path.basename(prune_stem or ""), log_dir, compress, plain=tiered,
|
||||||
)
|
)
|
||||||
rotator = make_rotator(
|
rotator = make_rotator(
|
||||||
compress, log_dir, prune_stem, backup_count, keep_uncompressed, keep_compressed,
|
compress, log_dir, prune_stem, backup_count, keep_uncompressed, keep_compressed,
|
||||||
|
rotate_mode=rotate_mode,
|
||||||
)
|
)
|
||||||
handler.namer = namer
|
handler.namer = namer
|
||||||
handler.rotator = rotator
|
handler.rotator = rotator
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ def _file_handler(
|
|||||||
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,
|
||||||
keep_uncompressed=keep_uncompressed, keep_compressed=keep_compressed,
|
keep_uncompressed=keep_uncompressed, keep_compressed=keep_compressed,
|
||||||
tiered=tiered,
|
tiered=tiered, rotate_mode=rotate,
|
||||||
)
|
)
|
||||||
elif rotate == "daily":
|
elif rotate == "daily":
|
||||||
handler = logging.handlers.TimedRotatingFileHandler(
|
handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
@@ -158,7 +158,7 @@ def _file_handler(
|
|||||||
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,
|
||||||
keep_uncompressed=keep_uncompressed, keep_compressed=keep_compressed,
|
keep_uncompressed=keep_uncompressed, keep_compressed=keep_compressed,
|
||||||
tiered=tiered,
|
tiered=tiered, rotate_mode=rotate,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if rotate == "on_start":
|
if rotate == "on_start":
|
||||||
|
|||||||
Reference in New Issue
Block a user