diff --git a/src/log_setup/rotation.py b/src/log_setup/rotation.py index 9f0088e..3209169 100644 --- a/src/log_setup/rotation.py +++ b/src/log_setup/rotation.py @@ -35,22 +35,6 @@ def _move(source: str, dest: str) -> None: shutil.move(source, dest) -def _free_dest(dest: str) -> str: - """return `dest`, or a `.N`-suffixed variant if it (or its .gz twin) already exists - - used by the tiered rotator so a second same-stamp roll doesn't clobber the earlier - file; checks both the plain and .gz forms of each candidate. - """ - if not os.path.exists(dest) and not os.path.exists(dest + ".gz"): - return dest - counter = 1 - while True: - candidate = f"{dest}.{counter}" - if not os.path.exists(candidate) and not os.path.exists(candidate + ".gz"): - return candidate - counter += 1 - - def _gzip_file(source: str, dest: str) -> None: """atomically gzip source into dest, then remove source - or leave source untouched @@ -169,10 +153,9 @@ def make_rotator( if not os.path.exists(source): return if tiered: - # strip the namer's .gz suffix so the roll lands plain and retier decides - # its tier; disambiguate a same-interval collision via _free_dest - plain_dest = _free_dest(dest[:-3] if dest.endswith(".gz") else dest) - _move(source, plain_dest) + # the tiered namer (plain=True) already minted a free, plain .log name; + # retier decides its tier + _move(source, dest) if log_dir is not None and prune_stem is not None: retier(log_dir, prune_stem, keep_uncompressed or 0, keep_compressed or 0, live_stem) return @@ -214,21 +197,8 @@ def rotate_on_start( tiered = keep_uncompressed is not None or keep_compressed is not None live_stem = os.path.splitext(os.path.basename(live_path))[0] stem = os.path.basename(history_stem) if history_stem else live_stem - stamp = time.strftime("%Y-%m-%d_%H-%M-%S", clock()) - suffix = ".log.gz" if (compress and not tiered) else ".log" - - # a 1-second stamp collision (rapid crash-restart) is disambiguated with a - # counter; check BOTH .log and .log.gz since a tiered same-stamp roll may - # already be compressed - def _taken(path: str) -> bool: - base = path[:-3] if path.endswith(".gz") else path - return os.path.exists(base) or os.path.exists(base + ".gz") - - dest = os.path.join(log_dir, f"{stem}.{stamp}{suffix}") - counter = 1 - while _taken(dest): - dest = os.path.join(log_dir, f"{stem}.{stamp}.{counter}{suffix}") - counter += 1 + # same name + same-second collision counter as the size/daily rolls + dest = make_history_namer(stem, log_dir, compress, plain=tiered, clock=clock)(live_path) if compress and not tiered: _gzip_file(live_path, dest) else: @@ -368,26 +338,13 @@ def prune(log_dir: str, stem: str, backup_count: int, live_stem: Optional[str] = (`.[.N].log[.gz]`, see `_is_rolled_name`) - a foreign file that only shares the `.` prefix (e.g. a project's own `.audit.log` dropped into the same log_dir) is left alone rather than counted and deleted as a roll. + + this is `retier` with no compressed tier (so a crash-left plain twin of a .gz is + dropped first, not counted); `backup_count <= 0` leaves history alone. """ if backup_count <= 0: return - stem = os.path.basename(stem) - live_stem = os.path.basename(live_stem) if live_stem else None - try: - entries = [ - os.path.join(log_dir, name) - for name in os.listdir(log_dir) - if name != f"{stem}.log" and _is_rolled_name(stem, name, live_stem) - ] - except OSError: - return - files = [(p, _safe_mtime(p), _roll_counter(p)) for p in entries if os.path.isfile(p)] - files.sort(key=lambda t: (t[1], t[2]), reverse=True) - for path, _, _ in files[backup_count:]: - try: - os.remove(path) - except OSError: - pass + retier(log_dir, stem, backup_count, 0, live_stem) def _safe_mtime(path: str) -> float: