Repository navigation
feat: BYO clients ship their own container logs; drop the CLI's detached shipper - #479
Merged
Merged
Conversation
…hed shipper BYO log shipping was host-side only: run-locally boxes relied on a detached CLI process that could die silently on operator laptops, and hand-off clients (register --no-run-locally, e.g. Run:AI workloads) had no shipper at all -- the container is its own PID 1 with no docker daemon to tail, so their allocator logs pages stayed empty. Now the container ships its own stream. start.sh routes fd 5 (every service's tagged output) through a new ship_logs worker in the client package when SHIP_LOGS=1, which register writes for every BYO shape: * Passthrough first: each line is written to container stdout before anything else touches it, so `docker logs` output is byte-identical; shipping happens on a separate thread fed by a bounded drop-oldest queue and can never block the read loop (the lablink#304 tail-stall lesson: no file intermediary, read the source stream). * Fail open: a supervisor loop respawns a crashed worker and execs plain `cat` after 3 failures -- worst case is exactly the old plumbing, never a frozen container. Missing env degrades to pure passthrough. The CLI's log_shipper.py (441 lines + 673 test lines), its PID/state files, register's spawn/revive machinery, Docker.follow_logs, and the psutil dependency are all deleted. doctor's shipper check becomes a pgrep inside the container. Release ordering: the client image must ship with the ship_logs entry point before a new CLI reaches operators, or BYO clients log locally but nothing reaches the allocator. Old images ignore SHIP_LOGS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzEx3SZp9EAmTVKe6JEWRa
CI's client coverage gate failed at 89% (fail-under=90): every worker primitive was tested but main() itself — env wiring, the shipper thread, signal-handler registration, the EOF final flush, and the missing-env passthrough degrade — was 34 uncovered statements (module at 66%). Two end-to-end tests through main() bring the module to 96%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzEx3SZp9EAmTVKe6JEWRa
7174Andy
added a commit
that referenced
this pull request
Sep 8, 2026
Release prep. publish-pip.yml's version guardrail rejects a tag whose version does not match pyproject.toml, so the bumps land on main before the release tags are cut. Allocator and client stay in lockstep at 0.4.0 as they have since 0.1.0; the CLI is versioned independently and goes to 0.3.0. The CLI's allocator pin is raised to >=0.4.0 this time: the CLI re-exports MachineConfig, whose ami_id default became empty (= resolve the per-region Deep Learning Base AMI, #489) in allocator 0.4.0. An older allocator would silently reintroduce the stale hardcoded us-west-2 AMI default that doctor's #490 fallback logic assumes gone. CHANGELOG (CLI): new 0.3.0 section (#484, #485, #490, #491, #498), and a backfilled 0.2.0 section — #481 tagged 0.2.0 without adding one (#467, #472, #474, #479). Also: README Docker <version> example moved to 0.4.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 8, 2026
Release prep. publish-pip.yml's version guardrail rejects a tag whose version does not match pyproject.toml, so the bumps land on main before the release tags are cut. Allocator and client stay in lockstep at 0.4.0 as they have since 0.1.0; the CLI is versioned independently and goes to 0.3.0. The CLI's allocator pin is raised to >=0.4.0 this time: the CLI re-exports MachineConfig, whose ami_id default became empty (= resolve the per-region Deep Learning Base AMI, #489) in allocator 0.4.0. An older allocator would silently reintroduce the stale hardcoded us-west-2 AMI default that doctor's #490 fallback logic assumes gone. CHANGELOG (CLI): new 0.3.0 section (#484, #485, #490, #491, #498), and a backfilled 0.2.0 section — #481 tagged 0.2.0 without adding one (#467, #472, #474, #479). Also: README Docker <version> example moved to 0.4.0. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ship_logsworker in the client package whenSHIP_LOGS=1, whichlablink client registerwrites for every BYO shape.register --no-run-locally, e.g. Run:AI workloads) had no log shipper at all — the container is its own PID 1 with no docker daemon to tail, so their allocator logs pages stayed permanently empty.log_shipper.py(441 lines + 673 test lines), its PID/state-file machinery,Docker.follow_logs, and thepsutildependency — net −1,284 lines.Changes Made
Client package
lablink_client_service/ship_logs.py+ship_logsconsole script: reads fd 5 from stdin, writes each line to container stdout first (sodocker logsoutput is byte-identical), queues a UTC-stamped copy in a bounded drop-oldest deque, and ships batches (50 lines / 15 s) toPOST /api/vm-logs/<VM_NAME>from a separate thread. SIGTERM/EOF do a single-attempt final flush so a gracefuldocker stopships the tail; missing env degrades to pure passthrough.start.sh: whenSHIP_LOGS=1, fd 5 feeds a supervisor loop that respawns a crashed worker (the kernel pipe buffer absorbs the gap) andexec cats after 3 failures — worst case is exactly today's plumbing, never a frozen container.CLI package
registerwritesSHIP_LOGS=1into the env file unconditionally (run-locally via--env-file, hand-off via the paste block); the shipper spawn/revive/stop machinery is deleted and_resumeshrinks to container-only.CONTAINER_NAMEmoves toregister.py.client doctor's shipper check becomesdocker exec lablink-client pgrep -f ship_logs(pgrep verified present in the published image).docs/reference/cli.md's "in-container shipper" wording is now literally accurate;docs/cli/byo-clients.mddocuments the hand-off paste requirement.Design Decisions
teea single point of failure for all container logging.docker logs/runai workspace logs, absent from the allocator, loudly visible either way.docker logsis the same stream the container sees internally.container-docker— the allocator's existing-dockersuffix routing lands it in thedocker_logscolumn with zero allocator changes.Release ordering
The client image must be published with the
ship_logsentry point before a new CLI release reaches operators — a new CLI plus an old image means BYO clients log locally but nothing reaches the allocator (old images simply ignoreSHIP_LOGS). AWS clients are untouched (user_data'slog_shipper.shremains the host-side shipper there, including cloud-init logs).Testing
test_ship_logs.py): passthrough byte-fidelity and ordering, timestamping, drop-oldest overflow, batch drain, retry/drop semantics, single-retry final flush, interval + stop flushes.SHIP_LOGS=1asserted in env file and hand-off printout, doctor's new exec-based check, resume path without shipper revival; stale shipper tests deleted.catfallback engaged.ship_logsbinary: clean passthrough with missing env and with an unreachable allocator.