Watch at EaseStories. Answers. Perspective.

Tech · PRACTICAL GUIDE

Python log rotation lost lines? Prove the inode switch and record union

Use one rotation owner, audit the open descriptor against the pathname, and verify numbered records exactly once across the rotated and active files.

Original editorial diagram comparing the log path and open descriptor device and inode before and after rename-and-create rotation.
Original editorial illustration: after the first post-rotation emit, the open descriptor must match the newly created pathname while the renamed file retains the old inode. Watch at Ease

A successful logrotate exit and a newly created app.log do not prove that a Python process stopped writing to the renamed inode. WatchedFileHandler checks the target device and inode before each emit on Unix-like systems, so verify that behavior at the descriptor, pathname and record levels.

Use one rotation owner. Pair WatchedFileHandler with external rename-and-create rotation, or use one internal rotating handler, but do not stack both mechanisms. Then number records around the boundary and accept the result only when the old and new files form one exact sequence without gaps or duplicates.

1. Choose exactly one rotation owner

For a service whose logrotate policy renames app.log and creates a new app.log, attach one WatchedFileHandler to the logger. Do not also attach RotatingFileHandler or TimedRotatingFileHandler to that same destination. The community duplicate-record case is a configuration conflict, not a reason to add a second watcher.

On Windows, or on a filesystem whose device and inode semantics do not behave like the tested local Unix filesystem, do not assume this method applies. Run the acceptance test on the real mount and process account before deployment.

2. Capture the descriptor and pathname identity

Immediately before rotation, flush the handler and record os.fstat(handler.stream.fileno()) for the open descriptor plus os.stat(log_path) for the pathname. Device and inode should identify the same object. Also record the service user, parent-directory permissions and the intended owner and mode of the replacement file.

This baseline distinguishes a stale descriptor from a missing or inaccessible replacement path. The CPython issue shows that reopen can fail when the target directory disappears; a rotation command succeeding cannot guarantee the application will reopen successfully.

3. Rotate by rename and create, then emit once

Rename app.log to app.log.1 and create a fresh app.log with the reviewed ownership and permissions before the next application write. The logrotate manual documents create as the step that makes a new file immediately after rotation. Keep the rotated file available until the audit finishes.

Emit one numbered sentinel after rotation and flush. Then compare the new pathname inode with the handler descriptor. Pass this layer only when they match and the rotated file still has the pre-rotation inode. If the descriptor still matches app.log.1, stop and fix the path, permissions or filesystem behavior.

4. Prove every numbered record exactly once

Write a bounded sequence before and after the boundary, such as seq-001 through seq-003 before rotation and seq-004 through seq-006 after it. Read both files completely. The union must equal the expected set, every expected value must occur once, and no unrecognized value may appear.

The local CPython 3.12.14 fixture placed seq-001 through seq-003 in the renamed inode and seq-004 through seq-006 in the new inode. Missing and duplicate sets were empty; the post-emit descriptor inode equaled the new pathname inode. Preserve the fixture output with the deployment evidence.

5. Reject shortcuts that hide the boundary

Do not treat copytruncate as an equivalent lossless fix. logrotate explicitly warns that data may be lost in the interval between copying and truncating. It may be a compatibility choice for a program that cannot reopen, but its documented loss window must be accepted and tested as a separate design.

Do not compare only total line counts. One missing record and one duplicate record can leave counts unchanged. Do not delete the rotated file before reading it, and do not infer success from the new path alone.

6. Record the limits of the result

This test proves the observed rename-and-create boundary for the tested Python version, filesystem, service account and configuration. It does not prove future rotations, remote filesystem semantics, container volume behavior or Windows compatibility.

Repeat the bounded test after changing Python, the handler, logrotate policy, mount type, permissions or service account. For production monitoring, alert on reopen exceptions and unexpected descriptor-to-path mismatches, then preserve both files for incident analysis.

Original editorial diagram showing sequence records one through three in the rotated file and four through six in the new active file.
Original editorial illustration: accept the rotation only when the rotated and active files form the exact numbered union with no missing or duplicate records. Watch at Ease