fix: prune/retier no longer treat a foreign same-stem file as a rolled log
prune() and retier() matched any file starting with "<stem>." (minus the live <stem>.log), with no check that the name actually has this lib's own rolled shape (<stem>.<stamp>[.N].log[.gz]). Since v0.5.0 the default history_stem is the cwd basename, so a project dropping its own same-stem file into log_dir (e.g. proj.audit.log, proj.stats.json) got silently deleted by prune once it aged past backup_count, or folded into retier's tier accounting (gzipped or deleted as if it were a real roll). Candidates are now filtered through _is_rolled_name before being treated as a roll. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -15,6 +15,7 @@ plain copy.
|
|||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional, Tuple
|
from typing import Callable, Optional, Tuple
|
||||||
@@ -252,12 +253,17 @@ def retier(log_dir: str, stem: str, keep_uncompressed: int, keep_compressed: int
|
|||||||
ordering is by mtime, then by the roll counter parsed from the name, so a
|
ordering is by mtime, then by the roll counter parsed from the name, so a
|
||||||
same-second burst (tied mtimes, counter-disambiguated stamps like run.<t>.log /
|
same-second burst (tied mtimes, counter-disambiguated stamps like run.<t>.log /
|
||||||
run.<t>.1.log) still tiers newest-first rather than falling back to listdir order.
|
run.<t>.1.log) still tiers newest-first rather than falling back to listdir order.
|
||||||
|
|
||||||
|
candidates are also checked against this lib's own rolled-file shape
|
||||||
|
(`<stem>.<stamp>[.N].log[.gz]`, see `_is_rolled_name`) - a foreign file that only
|
||||||
|
shares the `<stem>.` prefix (e.g. a project's own `<stem>.audit.log` dropped into
|
||||||
|
the same log_dir) is left alone rather than tiered/gzipped/deleted.
|
||||||
"""
|
"""
|
||||||
stem = os.path.basename(stem)
|
stem = os.path.basename(stem)
|
||||||
try:
|
try:
|
||||||
names = [
|
names = [
|
||||||
name for name in os.listdir(log_dir)
|
name for name in os.listdir(log_dir)
|
||||||
if name.startswith(f"{stem}.") and name != f"{stem}.log"
|
if name != f"{stem}.log" and _is_rolled_name(stem, name)
|
||||||
]
|
]
|
||||||
except OSError:
|
except OSError:
|
||||||
return
|
return
|
||||||
@@ -305,6 +311,23 @@ def retier(log_dir: str, stem: str, keep_uncompressed: int, keep_compressed: int
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _rolled_name_pattern(stem: str) -> "re.Pattern":
|
||||||
|
"""compiled regex matching this lib's own rolled-file shape for stem
|
||||||
|
|
||||||
|
`<stem>.<Y-m-d_H-M-S>[.N].log[.gz]` - the uniform namer output from
|
||||||
|
make_history_namer/rotate_on_start (see attach_rolling). used to filter
|
||||||
|
prune/retier candidates so a foreign file merely sharing the `<stem>.` prefix
|
||||||
|
(e.g. a project's own `proj.audit.log` sitting beside its `proj.<stamp>.log`
|
||||||
|
rolls) is never mistaken for a roll and pruned/retiered/deleted.
|
||||||
|
"""
|
||||||
|
return re.compile(rf"^{re.escape(stem)}\.\d{{4}}-\d{{2}}-\d{{2}}_\d{{2}}-\d{{2}}-\d{{2}}(?:\.\d+)?\.log(?:\.gz)?$")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_rolled_name(stem: str, name: str) -> bool:
|
||||||
|
"""return whether name has this lib's own rolled-log shape for stem"""
|
||||||
|
return _rolled_name_pattern(stem).match(name) is not None
|
||||||
|
|
||||||
|
|
||||||
def _roll_counter(path: str) -> int:
|
def _roll_counter(path: str) -> int:
|
||||||
"""parse the same-second disambiguation counter out of a rolled filename
|
"""parse the same-second disambiguation counter out of a rolled filename
|
||||||
|
|
||||||
@@ -331,6 +354,11 @@ def prune(log_dir: str, stem: str, backup_count: int) -> None:
|
|||||||
FOOTGUN: `stem` is reduced to its basename so a `name` containing a directory (e.g.
|
FOOTGUN: `stem` is reduced to its basename so a `name` containing a directory (e.g.
|
||||||
"sub/run") still matches the basenamed rolled files in log_dir - else nothing
|
"sub/run") still matches the basenamed rolled files in log_dir - else nothing
|
||||||
matches and old files pile up forever.
|
matches and old files pile up forever.
|
||||||
|
|
||||||
|
candidates are also checked against this lib's own rolled-file shape
|
||||||
|
(`<stem>.<stamp>[.N].log[.gz]`, see `_is_rolled_name`) - a foreign file that only
|
||||||
|
shares the `<stem>.` prefix (e.g. a project's own `<stem>.audit.log` dropped into
|
||||||
|
the same log_dir) is left alone rather than counted and deleted as a roll.
|
||||||
"""
|
"""
|
||||||
if backup_count <= 0:
|
if backup_count <= 0:
|
||||||
return
|
return
|
||||||
@@ -339,7 +367,7 @@ def prune(log_dir: str, stem: str, backup_count: int) -> None:
|
|||||||
entries = [
|
entries = [
|
||||||
os.path.join(log_dir, name)
|
os.path.join(log_dir, name)
|
||||||
for name in os.listdir(log_dir)
|
for name in os.listdir(log_dir)
|
||||||
if name.startswith(f"{stem}.") and name != f"{stem}.log"
|
if name != f"{stem}.log" and _is_rolled_name(stem, name)
|
||||||
]
|
]
|
||||||
except OSError:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user