Unread rotated logs can be lost through a relative symlink

perfloop/victorialogs · VALUE KIND CONFUSION

https://perfloop.ai/t/oss/case_ywvx2gxjg1

Verdict

VERIFIED · settled 2026-10-01

What happened: The assertion is violated on the comparison and satisfied with this change.

Hypothesis

Restart recovery can discard unread lines from a rotated log if the configured log path is a relative symlink. The code may search from the process working directory instead of the symlink's directory. Whether deployed file layouts trigger this is unmeasured.

At revision 6cdc3d6, `filecollector.startRead` passes a matched `-fileCollector.glob` file path to `Tailer.StartRead`, which makes the link path absolute without resolving it. When a saved checkpoint's inode differs from the new file at that path, `tryResumeFromCheckpoint` calls `findRenamedFile`. Its `tryResolveSymlink` returns the raw `os.Readlink` target; `findRenamedFile` applies `path.Dir` to that string and scans the result. For `links/app.log -> ../physical/app.log`, this searches `../physical` relative to the working directory, not relative to `links`. If the old inode remains in the physical directory but that wrong directory is absent or lacks the inode, `openLogFile` deletes the checkpoint and starts at the new file, so the unread old lines are not recovered. This claim requires the link still to point into the rotated file's physical directory, a saved offset and fingerprint for the old file, and rename-create rotation while vlagent is stopped; it does not claim recovery when the old file is removed or compressed.

`TestTailer` covers restart after rename-create, but `createTestLogFile` constructs an absolute-target symlink. A Case can reproduce the distinct relative-target condition with a focused `Tailer.StartRead` restart: checkpoint after an initial row, append an unread old row during shutdown, rename the physical file and create a new one, then verify both the old row and new row arrive in order after restart. Keep direct-path and absolute-target symlinks as controls, and check that the saved checkpoint advances to the new file. The test failing only for the relative target before the change and passing after it would confirm this loss path; successful recovery before the change would refute it.

Change to test: Search for a rotated file in the symlink target's physical directory, resolving relative and chained targets against their links while keeping the logical checkpoint key and inode/fingerprint checks. Add restart coverage for a relative target in another directory.

Where it lives

perfloop/victorialogs · app/vlagent/tail/tailer.go

Evidence

The assertion is violated on the comparison and satisfied with this change: `For a direct log path or an absolute, relative, or chained relative symlink whose checkpoint records the old file offset and fingerprint, when rename-create rotation occurs while the tailer is stopped, the uncompressed old file remains readable in a target directory the tailer can list, and the supplied Processor returns true from TryAddLine for the replacement-file line, restarting Tailer.StartRead must deliver the unread old line before the replacement-file line and persist the replacement inode, fingerprint, and offset under the unchanged logical path.`

Checks: 9 of 9 passed. Verification: no defect found.

Timeline