Skip to content

pgremote: follow the log instead of stopping at its current end - #6

Merged
pashagolub merged 3 commits into
mainfrom
feat/pgremote-follow
Sep 2, 2026
Merged

pgremote: follow the log instead of stopping at its current end#6
pashagolub merged 3 commits into
mainfrom
feat/pgremote-follow

Conversation

@pashagolub

Copy link
Copy Markdown
Contributor

What

Adds Follow and PollInterval to pgremote.Config, so the remote reader keeps reading instead of ending at the last byte the server had written when Open was called.

FileSet has had both for the local path all along. The remote path did not, and nothing in either API said so.

Why

Found by running the pgwatch docker-compose deployment against three PostgreSQL instances, one per log_destination.

server_log_event_counts was written once per source and then never again, while every other metric kept flowing:

metric last write
server_log_event_counts 15:55:40 — frozen
db_stats (control) 15:58:41 — still flowing

The csvlog file grew 318 KB → 949 KB in that window, all of it unread.

The chain: pgremote returns io.EOF once it drains the files → pgwatch's consume returns → parseStream sends one measurement and returns nilrunLogParser treats a clean return as success and never calls again. No error is logged anywhere, so the only symptom is a metric that stops.

This is a regression from the pglogwatch migration — the pre-migration parseLogsRemote had an infinite poll loop over pg_ls_logdir. Nothing replaced it. The local path was unaffected because openLocal already passed Follow: true.

Two cases follow mode has to get right

A half-written line is not a record. Reaching the end of what the server has written is not reaching the end of the file. A trailing line with no newline is the file's last line to a one-shot reader (FMT-009), and a record still being written to a follower. Flushing it would split one log record in two — half delivered now, half prepended to the next poll's chunk — with both halves parsing as malformed and the event counted zero times or twice. A follower rewinds past the partial line and drops it; the recorded offset points at its first byte, so the next poll re-reads it whole.

Cancellation means different things to the two readers. A one-shot reader was going to finish on its own, so being cancelled cost the caller data and is an error. A follower never finishes on its own — cancelling it is the only way to stop it. A cancelled follower now ends at io.EOF, matching FileSet, rather than making every caller special-case the normal shutdown path.

Note for reviewers

Config.Offsets is documented as "nil means no persistence". For a follower that would mean re-delivering every file on every poll, since a follower re-lists the directory and decides what is new from the stored offsets. Open now substitutes a bounded in-memory store when Follow is set. Behaviour with Follow false is unchanged.

relist deliberately does nothing but replace the file slice and rewind the index — every decision about what to read next already lives in nextFile, which compares each listed size against the stored offset. The first pass and the follow path take the same decisions from the same evidence.

Testing

  • 5 new tests in pgremote/follow_test.go, asserting on the reader's bytes rather than parsed records — what follow mode changes is framing, and a test that only counted records would pass while the counts it produced were wrong
  • go test -race ./... green in this module and the root module; existing tests unchanged
  • Verified live against the pgwatch stack, three instances, repeated collection cycles: csvlog 28 errors, jsonlog 51, stderr 50 total

Follow-up

Needs a pgremote/v1.0.1 tag; pgwatch currently carries a temporary replace pointing here.

🤖 Generated with Claude Code

pashagolub and others added 2 commits September 2, 2026 15:56
Open returned a reader that ended at the last byte the server had
written when it was called. That is right for a one-shot read and wrong
for a log reader: pgwatch's consume returned, parseStream sent one
measurement and returned, and runLogParser read that clean return as
success and never called again. Log parsing reported once per pgwatch
start and then went silent, while every other metric kept flowing -- a
failure with no error anywhere to notice it by.

FileSet had Follow and PollInterval for the local path all along. This
adds them to the remote one, so a caller can switch between the two
readers without one of them quietly being a different kind of reader.

Following needs somewhere to record progress: a follower re-lists the
directory on every poll and decides what is new from the stored offsets,
so with Config.Offsets nil it would deliver the whole directory again on
every pass. Open now substitutes a bounded in-memory store when Follow
is set, which is what makes the flag mean what it says.

Two cases follow mode has to get right that a one-shot reader does not:

Reaching the end of what the server has written is not reaching the end
of the file. A trailing line with no newline is the file's last line to
a one-shot reader (FMT-009) and a record still being written to a
follower. Flushing it would split one log record into two, each half
parsing as malformed, and the event it describes counted zero times or
twice. A follower rewinds past the partial line and drops it; the
recorded offset points at its first byte and the next poll re-reads it
whole.

Cancellation is an error to a reader that was going to finish on its own
and the designed stop signal to one that never will. A cancelled
follower now ends at io.EOF like any exhausted reader, matching
FileSet, rather than making every caller special-case a normal shutdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pashagolub pashagolub self-assigned this Sep 2, 2026
@pashagolub pashagolub added the enhancement New feature or request label Sep 2, 2026
Trailing comments on two adjacent short-variable declarations were
aligned by hand, which gofmt undoes. Caught by the pgremote lint job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pashagolub
pashagolub merged commit e2958a6 into main Sep 2, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant