Skip to content

feat(terminal): read from where the panel got to, so no log line is missed - #40

Open
MorningLightMountain713 wants to merge 3 commits into
masterfrom
feat/log-cursor
Open

feat(terminal): read from where the panel got to, so no log line is missed#40
MorningLightMountain713 wants to merge 3 commits into
masterfrom
feat/log-cursor

Conversation

@MorningLightMountain713

@MorningLightMountain713 MorningLightMountain713 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Reads an app's logs from where the viewer got to, so no line written between two polls is missed — and drops the response time of every poll from ~1500 ms to milliseconds.

Needs the FluxOS side first: RunOnFlux/flux#1794. Works unchanged against every node that does not have it yet — see Backwards compatibility below.

The problem

The viewer asked for the last N lines every few seconds and replaced its view with them. Anything an app wrote beyond N between two polls was never shown to anyone and could not be fetched afterwards.

since did not help. It was a box a user typed into, it never advanced, and docker applies tail after since anyway — so a burst of 500 lines with tail: 100 answers a reader asking for "everything since T" with the last 100 and no sign the rest existed.

What changes

The panel takes whichever of three things the node it is talking to can offer, decided per node and per request with no version check anywhere.

A node that streams pushes lines over /applogs as the server writes them. There is no tick and no position arithmetic — a stream has no gap between polls to lose lines in. The poll stands down only once the node has answered subscribed, and resumes if the stream ends or fails, so a node without the namespace is never left unpolled.

A node that answers positions hands back an opaque position; the panel hands it back and is answered with only what came after, appending instead of replacing.

A node that does neither returns no cursor, and against it the panel behaves exactly as it does today.

Markers rather than silent gaps:

  • rolled over — docker discarded the file holding the line the panel had read up to, so those lines are gone for everyone.
  • skipped — the panel fell further behind than one read reaches, or the server wrote faster than the connection drained. Those lines still exist; the node declined to walk back to them, because reaching them costs a read of the whole retained log on every poll.

The position is never read here. Nodes and this app upgrade independently, so parsing it would make its shape a permanent contract. It is sent as a query parameter, because the route takes three optional path segments and a fourth would 404 on a node that predates this.

There is no drain loop. A node answers a position with everything waiting for it, so there is never a second page to come straight back for.

A position belongs to one node's container. The same app on another node has its own log with its own timestamps, so carrying a position across would ask that node about a moment that means nothing there and then skip lines to match a count taken from somewhere else. It is dropped whenever the target changes.

Backwards compatibility

A node that does not answer positions returns no cursor, and against that node this behaves exactly as it does today. A node with no /applogs refuses the connection and the poll simply carries on. No version check anywhere — the presence of the field, and the success of the connection, are the capability signals, decided per node and per request.

The network runs several FluxOS versions at once and always will, so both fallbacks are permanent rather than transitional.

Deployment order

  1. fix(apps): a missing container must not crash FluxOS, a failed redeploy must not uninstall the app, and a log poll must lose nothing flux#1794 — the node side. Additive: a request without a cursor behaves exactly as before. It is also useful on its own, since the 1500 ms and the log-retention fixes reach every existing client with no frontend change at all.
  2. This PR, and its siblings, in any order and independently of each other.

The reverse order does nothing harmful — a viewer shipped first would send a cursor every node ignores, get no cursor back, and sit in the fallback path — but there is no reason to do it.

Sibling PRs

The same change lands in three viewers, because all three had the same defect:

Testing

Proved end to end in a browser: this branch served by a dev server, pointed at a real node running flux#1794, against an app deployed for the purpose writing a numbered line every second — numbered so a gap and a repeat are both visible in the sequence.

The stream, proved from both ends at once. The live toggle is the only variable:

live on live off
lines delivered to the panel 15 in 15 s, matching the container's 1/sec 0 in 12 s
HTTP requests of any kind 0 0
the node's open docker connections 2 1

Polling cannot deliver a line without HTTP, and at the fallback interval there would have been several requests; there were none. The extra docker connection appears when the viewer subscribes and goes when it leaves — the node's follow stream, observed on the node itself.

233 lines contiguous, 0 duplicates, 0 gaps, appending rather than replacing, well past the line limit the replace behaviour could never exceed.

The fallback still answers. Against that same node, every request shape a client without streaming sends: …/100 → 100 lines truncated: true; …/all → 105,146 lines truncated: false; …/100/<date> → 100 lines truncated: true; and a cursor four seconds old → exactly 4 lines.

The node side carries its own unit and fleet coverage — 98 suites, 0 failures; see flux#1794.

🤖 Generated with Claude Code

…issed

The panel asked for the last 100 lines every ten seconds and replaced its view
with them, so a server writing more than 100 lines between two polls lost the
rest with nothing to show it had happened. serverReadiness already recorded the
consequence in its own comment - "applogpolling returns the tail of the log, so
both are best-effort by nature".

The node now hands back a position and answers it with only what came after, so
the panel appends rather than replaces, and drains immediately when the node
says more was waiting than one answer carried.

The position is opaque and is never read here: nodes and this site upgrade
independently, so reading it would make its shape a contract. It is sent as a
query parameter because the route takes three optional path segments and a
fourth would 404 on a node that predates this.

A node that does not answer positions returns no cursor, and against that node
the panel keeps doing exactly what it does today.

A position belongs to ONE node's container - the same server on another node has
its own timestamps - so it is dropped whenever masterIp or the container name
changes.

serverReadiness is deliberately untouched: it asks whether a server has finished
starting, which is a tail question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hind the line count

The node answers two questions. `hasMore` is what lies ahead of the position
just stored - the only thing another pass can fetch. `truncated` is the log
holding more than the line count asked for, which is behind this panel and
unreachable with a cursor, and which is true on nearly every first page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he poll when it does not

A node that carries /applogs pushes lines as the server writes them, so the
panel has no ten-second tick and no position arithmetic - a stream has no gap
between polls to lose lines in. A node that predates it refuses the namespace
and the poll simply carries on, which is permanent rather than transitional:
the network runs several FluxOS versions at once and always will.

The poll stands down only once the node has answered `subscribed`, so a node
without the namespace is never left unpolled, and it resumes on a stream that
ends or fails.

The drain loop goes. A node answers a position with everything waiting for it,
so there is never a second page to come straight back for - a panel further
behind than one answer holds is moved to the end of the log and told, and no
number of extra passes would reach the lines it went past. `skipped` says so,
alongside the rolled-over notice where the lines are gone rather than declined.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant