Repository navigation
feat: logs and logs --follow with WebSocket reconnect and gap-fill - #13
Open
RogerCecilpot wants to merge 1 commit into
Open
RogerCecilpot wants to merge 1 commit into
RogerCecilpot wants to merge 1 commit into
Conversation
`devlyft logs <deploy-id>` prints recent lines from the REST endpoint; --follow then streams new lines from logs-api's live-tail WebSocket until interrupted. Flags: --since, --pod, --filter, --match, --limit, --timestamps. --format json prints NDJSON and --jq applies per line. internal/api/logstream holds the hand-written client: - Dial authenticates with the token in the bearer subprotocol (never the URL), maps rejected handshakes to clierr (401/403 exit 3, 404 exit 4) and logs the handshake through redaction. - Follow reconnects after the server's 4001 re-auth close with a fresh token, backs off drops and failed dials with full jitter, and after every connect fills the gap from REST `since` (a duration on this API), de-duplicating by timestamp, pod and line. A 4001 that arrives at once is backed off, since the server also sends it when its tail fails. The facade gains FollowLogs, built on the same http.Client, token source and logger as REST calls so proxy and CA settings apply to the stream. output.Printer gains PrintRecord for streamed records. Refs devlyft/devlyft#254 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RogerCecilpot
changed the base branch from
feat/250-test-harness
to
feat/244-config-contexts
October 25, 2026 18:59
RogerCecilpot
force-pushed
the
feat/254-logs
branch
from
October 25, 2026 18:59
bf636b2 to
689430e
Compare
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.
Closes devlyft/devlyft#254.
Stacked on #11 (#250). It will be restacked onto #244 (config and contexts) once that PR exists.
What
devlyft logs <deploy-id>prints recent lines fromGET /v1/deploys/{id}/logs, oldest first.--follow/-fthen streams new lines until Ctrl-C (exit 130).--since(a Go duration; server default 1h),--pod,--filter,--match(repeatable),--limit(at most 5000),--timestamps.timestamp\tpod\tline;--format jsonprints NDJSON, oneLogEntryper line; YAML prints one document per line;--jqapplies to each line;--quietis a usage error.--jq, reconnect notices go to the debug log, so stderr only ever carries the error object.internal/api/logstreamis the hand-written live-tail client:Dialuses github.com/coder/websocket. The token goes in the bearer subprotocol, never the URL; the handshake is debug-logged throughredact. Rejected handshakes becomeclierrerrors: 401 and 403 exit 3, 404 exits 4, and none are retried.Followis the reconnect loop, independent of the WebSocket library:api.Client.FollowLogsuses the same*http.Client, token source, user agent and logger as REST calls, so #244's proxy and CA bundle settings will apply to the WebSocket with no extra wiring. The gomock mock is regenerated.output.Printer.PrintRecordprints streamed records: NDJSON, YAML documents,--jqper record, TSV, or text.docs/logs.mddocuments the flags, the client contract and the gap-fill algorithm; the README has a short Logs section.WebSocket contract (from logs-api's stream handler)
GET /v1/deploys/{deployId}/logs/stream, with query parameterspod,filterandmatch(repeatable), the same filters as REST.Sec-WebSocket-Protocol: bearer, <token>, and the server selectsbearer. coder/websocket sendsbearer,<token>, which the server accepts because it splits on commas and trims.{"timestamp","line","pod"?}: the same shape as the RESTLogEntry.since: the tail starts at the moment of connection.reauthenticateafter at most 30 minutes. It also sends 4001 whenever its tail ends, including on failures.Decisions
sinceon REST is a duration, not a timestamp: the spec says it's a Go duration such as1h. The gap-fill asks forsince = now − newest line shown + 5sof slack, rounded up to whole seconds, withlimit=5000. If the page comes back full, a notice says some lines may be missing.*http.Clientfor the handshake, which is what makes the proxy and CA settings apply.Tests
logstream.Follow, against fakes:logstream.Dial, against a real WebSocket test server: the subprotocol token, the token never in the URL, path, query and user agent, debug logs without the token, 4001 mapped toErrReauth, other closes, binary frames skipped, malformed frames, every rejected-handshake status and its exit code, network and token-source errors, and URL scheme mapping.apitest). The first stream sends a line and closes 4001 immediately. The client reconnects with a different token, and both gap-fills are validated against the vendored spec (sincein seconds,limit=5000). Every line is emitted exactly once, in order.--timestamps, filters passed through, follow (text and JSON), interrupted (exit 130), stream 403 (exit 3), 404 (exit 4), and usage errors (exit 2).make lintreports 0 issues, andmake testandmake vulnpass.make cover-checkreports 87.6% total (logstream 96.7%, the logs command 96.2%). The Windows and macOS cross-builds and actionlint are clean.Not done here
docs/logs.mdholds the client-side contract until it is.🤖 Generated with Claude Code