The path
- The worker writes every line to the container’s log file as it arrives, and numbers it. The file is the source of truth; nothing is held in memory for readers.
- A reader tails that file from a line number. The first attach starts at line one; a reader that comes back after a drop sends the last line it saw and gets what it missed. Two readers, ten readers, each see everything.
- The controlplane passes the line number through on
AttachToContainerandJobInvoke, so the CLI and the dashboard both resume rather than replay. - The dashboard spools to disk and records its position next to the file. A finished container’s output stays readable after the container is gone.
Before this, the worker kept output in one in-memory buffer that handed each line to whichever reader
asked first. With the CLI and the dashboard both attached, each saw half the lines (step 1, 6, 31 in
one, step 16, 36, 46 in the other), and a reader that arrived late saw nothing. That is what the numbered
file replaced.
Reading logs
- CLI:
python app.pystreams the job’s output as it runs. If the connection drops, the CLI reattaches to the same task and continues from the last line it printed. - Dashboard: a call’s drawer shows its output live and keeps it afterwards; Full view opens the whole stream; Download gives the entire spool file, not just the last screenful.
- A long run opens at its newest part. A training job can leave hundreds of thousands of lines, so the
viewer starts from the newest 64 KB and keeps the newest 3,000 lines once they are parsed, which is what
paints at once. It says which of the two bounds is in force: showing the newest part of 344 KB · show
everything re-reads from the first line, and showing the newest 3,000 lines · download all of it
hands over the whole spool file. Neither is a gap in the capture: the spool has every line either way,
and
Downloadalways gives the lot. - Before the first line, the viewer says what the container is doing (waiting to be scheduled, running but nothing printed yet). A container that is loading its blueprint or importing a large library can be quiet for a minute or two; that is the container, not the stream.
How long logs are kept
A year, on the dashboard’s own volume, set byLEVERS9_LOG_RETENTION_DAYS. A container’s spool is
removed once it is older than that, and the spool of a container still being captured is never removed
however old its file looks — an always-on job that has been quiet carries the timestamp of its last line.
LEVERS9_LOG_SPOOL_MAX_MB (8 GB) is a backstop rather than a policy: if the spool grows past it, the oldest
go early and the dashboard log says so, because a spool removed because the volume was filling is a different
fact from one that aged out.
This used to be a count — the newest 500 spools, whatever span that covered. So how far back the logs went
depended on how many containers had run rather than on time: a busy week bought a few days and a quiet one
bought a month, with no way to tell which you had. The prune also ran only when the process started, so the
spool was cut back at each deploy rather than ageing out, which is why logs seemed to disappear in steps.
Keeping them longer than that
The spool is one disk attached to one pod. It survives a restart and not the loss of that disk, and a year of retention on a single disk is still a single disk. So a workspace can ask for its finished logs to be kept: Members → Keep container logs in this workspace. With it on, a container’s output is copied to acontainer-logs volume in that workspace once the
container finishes, and stays there after the local spool ages out — the log view falls back to the archived
copy without anyone asking it to. With it off, logs live on the dashboard’s disk for the retention window
above and are then gone.
It is off until a workspace admin turns it on, and turning it on creates the volume. That is storage the
workspace owns and pays for, holding every container’s output indefinitely — a decision to take rather than
inherit, which is why there is no deployment-wide default that does it for you.
What can go wrong
Where to look on a worker
Each container’s file is/var/log/worker/<container id>.log on the worker that ran it: one JSON record
per line, with msg, time, container_id and task_id. levers9 worker list shows which worker.
