Skip to content
Shell & TerminalDeep Dive Published Updated 6 min readViews unavailable

GNU tail in Shell: Follow File Names Across Log Rotation

Use GNU tail's descriptor and name-follow modes to survive log rotation, retry missing files, and bound a follow with an optional writer PID.

tail -f looks like a simple way to watch a log, but “follow” can mean two different things. GNU tail follows an open file descriptor by default: it keeps reading the same file object even after the pathname has been renamed. Log rotation often needs the other behavior, following the pathname and reopening it when a replacement appears. Confusing these modes can leave an operator watching yesterday’s renamed log while the application writes to a new file.

The distinction is especially important for scripts and incident response. Decide whether the target is the current file object or the current object named by a path, then choose options that describe that contract. GNU extensions differ from some BSD/macOS tail implementations; on macOS, Homebrew Coreutils commonly installs the GNU command as gtail, while the system /usr/bin/tail is not GNU Coreutils.

Descriptor-follow versus name-follow

The short option -f defaults to --follow=descriptor. tail opens a file and continues reading that open descriptor. If a rotation tool renames the file and the writer continues writing to the old inode, descriptor-follow can keep showing those writes. It will not automatically switch to the new file merely because the original path now names another object.

--follow=name instead tracks the pathname and reopens it when the file at that name changes. GNU -F is shorthand for --follow=name --retry: it keeps trying to open the named file if it is missing or inaccessible, which is useful during rename-and-create rotation.

# Follow the file object that was opened, even if its name changes.
gtail --follow=descriptor -- ./var/log/app.log

# Follow the path, retrying across a missing-file rotation window.
gtail -F -- ./var/log/app.log

On GNU/Linux, replace gtail with tail from GNU Coreutils. The -- marks the end of options so a path beginning with - is not parsed as an option. A leading -n 0 is useful when attaching to a log because it suppresses the existing history and prints only data observed after startup:

gtail -n 0 -F -- ./var/log/app.log

This is still a stream of bytes divided into lines, not a structured log subscription. If the file contains partial lines, tail can print them as more bytes arrive. Use an application-level parser for framing, correlation IDs, or delivery guarantees.

Understand the rotation strategy

Rename-and-create rotation renames the active file and creates a new file at the original path. A descriptor follower remains on the renamed object; a name follower can reopen the replacement. -F is usually the straightforward observer when an external rotator performs this handoff, but the exact gap and how quickly the new file is noticed depend on the system’s notification or polling behavior.

copytruncate works differently: the rotation process copies the file and then truncates the original object instead of replacing its pathname. Since the descriptor still refers to the same object, descriptor-follow can continue after observing a shrink. The logrotate documentation warns that the interval between copying and truncating can lose logging data: records appended after the copy snapshot but before truncation may be erased without reaching the archive. This is a rotation-protocol limitation, not a claim that tail duplicates output. If complete logs matter, prefer a rotation protocol where the writer reopens the destination or coordinates a clean handoff.

GNU tail can use file-system notifications where available; otherwise it polls. --sleep-interval=N controls approximate polling cadence, with a default of one second, but inotify-based following generally ignores the sleep interval for change notification. A name follower may periodically re-check file identity after unchanged-size iterations; --max-unchanged-stats=N adjusts that behavior in polling mode. Faster polling can reduce detection delay but costs wakeups, and it does not make rotation atomic.

If the target may not exist when monitoring begins, -F retries. It does not create the file, repair permissions, or ensure the application will eventually write it. For diagnostics, report command errors separately from “no new data”: silence alone cannot distinguish an idle writer from a missing path, stale descriptor, or permission failure.

Bound a follow to the process that writes

For a short-lived command that creates or updates a log, GNU --pid=PID asks tail to exit shortly after the specified writer process disappears. It is an operational bound, not an exact completion notification. The PID must be the process whose lifetime truly controls writes: choosing a parent that outlives the writer can wait too long, while choosing a helper that exits early can stop observation too soon.

producer --output=./var/log/one-shot.log &
writer_pid=$!
gtail -n 0 --follow=name --retry --pid="$writer_pid" \
  -- ./var/log/one-shot.log
wait "$writer_pid"

The order above starts the producer and then attaches a follower, so a very fast writer could finish before tail opens the path. For a strict capture requirement, have the producer create the file before announcing readiness, or make the supervisor own both process startup and output capture. tail is a viewer; it is not a substitute for redirecting the producer’s output to a durable file or pipe.

GNU --pid is a GNU extension and may not exist in the platform’s native tail. It checks process existence at intervals, so exit can be observed with a small delay. Avoid reusing a stale PID file: a recycled PID may refer to a different process. For a supervisor that already knows the child’s lifecycle, wait on the actual child process and define what “all output drained” means before exiting.

Verify which implementation your script will run

Before relying on GNU-only behavior, test the executable rather than assuming that the name tail maps to GNU Coreutils:

tail --version       # GNU Coreutils/Linux installations
gtail --version      # common Homebrew Coreutils command on macOS
command -v tail gtail

If a portable script needs to follow a file across rotation, declare the GNU dependency or provide an implementation-specific branch with tests. Do not silently assume -F, --pid, or GNU long options exist on every Unix-like system. Preserve the path and writer lifecycle in logs so an operator can tell which file and executable were monitored.

Test rotation with a temporary directory: open a writer descriptor to the active file and keep it open, then start one follower in descriptor mode and one in name mode. Rename the active file, create a replacement at the original path, write one distinct marker through the writer’s still-open descriptor and another through a new writer to the replacement, then compare output. The descriptor follower should report the renamed object’s marker; the name follower should move to the replacement path. Repeat with a temporarily absent path and with truncation, and verify the expected output instead of judging by whether a terminal appears active.

Treat the output as observation, not retention

tail does not archive, lock, or snapshot a log. Rotation can race with reading; the process may be restarted; permissions may change; and standard output can itself fail or be disconnected. When logs are evidence or required records, configure the application and rotation system to preserve them with explicit retention and delivery policy. Use tail to inspect a stream whose producer and rotation behavior you understand.

Related:

Sources:

Comments