Add sc logs — follow a running service's output #12

Merged
geusebio merged 1 commit from feature/sc-logs into main 2026-08-04 21:32:59 +00:00
Owner

The CLI half of log tailing. The platform serves the tail as newline-delimited JSON on the bearer-token firewall; this reads it.

sc logs web              # last 200 lines, then follow
sc logs web --tail 0     # from now, replaying nothing
sc logs web > web.log    # only log lines land in the file

Against a live platform:

$ sc logs --app test-webserver --tail 3 db
Following test-webserver/db on http://localhost:7739          ← stderr
2026-08-04 15:24:12.482 UTC [1] LOG:  database system is ready to accept connections
2026-08-04 15:29:12.518 UTC [72] LOG:  checkpoint starting: time
2026-08-04 15:29:16.907 UTC [72] LOG:  checkpoint complete: wrote 43 buffers …
The log stream ended.                                          ← stderr

Decisions worth reviewing

stdout is log lines and nothing else. Which application, which endpoint, why the stream ended — all stderr, so redirecting collects the log clean. There's a test asserting the keep-alive reaches neither stream.

The application resolves exactly as deploy resolves it--app, then .sc.yml, then the directory name — so the two cannot mean different applications in one checkout.

The streaming request does not use the shared http.Client. Its five-minute ceiling is exactly wrong for a tail, whose job is to stay open and say nothing for long stretches; the deadline that matters is the context's. Tested, because the failure mode is a tail that dies after five minutes and nobody notices until they leave one running.

main now installs a signal handler and runs the command with that context. Ctrl-C is how a person stops watching a log, so cancellation is reported as a quiet end rather than an error. Every other command benefits too: a request is now actually abandoned rather than left for the platform to answer into a dead socket.

Unparseable frames are skipped, not fatal. A newer platform sending something this client doesn't know shouldn't cost the reader the lines either side of it.

APIError gains RetryAfter, so a full stream pool is reported as "try again in 10s" rather than "try again".

Depends on

The platform-side endpoint GET /cli/applications/{slug}/services/{service}/logsSomeones.Computer#155. Against a platform without it, sc logs says "…cannot stream logs — sc logs needs a newer platform" rather than "not found", because a 404 with no error key is a missing route, not a missing application. There's a test for that too.

Verification

  • go vet ./..., go test ./... — clean.
  • Cross-compiled linux/amd64, darwin/arm64, windows/amd64 (the CI matrix's shape).
  • Run against a live platform, not only stubs: the transcript above is a real Postgres service, and the 409 (no such service), 404 (no such application) and missing-token paths were checked through the built binary.

19 new tests — 11 on the client (framing, ping, escaping, cancellation, the timeout, each error shape) and 8 on the command (stream separation, the flags, and each error turned into a sentence someone can act on).

The CLI half of log tailing. The platform serves the tail as newline-delimited JSON on the bearer-token firewall; this reads it. ```sh sc logs web # last 200 lines, then follow sc logs web --tail 0 # from now, replaying nothing sc logs web > web.log # only log lines land in the file ``` Against a live platform: ``` $ sc logs --app test-webserver --tail 3 db Following test-webserver/db on http://localhost:7739 ← stderr 2026-08-04 15:24:12.482 UTC [1] LOG: database system is ready to accept connections 2026-08-04 15:29:12.518 UTC [72] LOG: checkpoint starting: time 2026-08-04 15:29:16.907 UTC [72] LOG: checkpoint complete: wrote 43 buffers … The log stream ended. ← stderr ``` ## Decisions worth reviewing **stdout is log lines and nothing else.** Which application, which endpoint, why the stream ended — all stderr, so redirecting collects the log clean. There's a test asserting the keep-alive reaches neither stream. **The application resolves exactly as `deploy` resolves it** — `--app`, then `.sc.yml`, then the directory name — so the two cannot mean different applications in one checkout. **The streaming request does not use the shared `http.Client`.** Its five-minute ceiling is exactly wrong for a tail, whose job is to stay open and say nothing for long stretches; the deadline that matters is the context's. Tested, because the failure mode is a tail that dies after five minutes and nobody notices until they leave one running. **`main` now installs a signal handler** and runs the command with that context. Ctrl-C is how a person stops watching a log, so cancellation is reported as a quiet end rather than an error. Every other command benefits too: a request is now actually abandoned rather than left for the platform to answer into a dead socket. **Unparseable frames are skipped, not fatal.** A newer platform sending something this client doesn't know shouldn't cost the reader the lines either side of it. `APIError` gains `RetryAfter`, so a full stream pool is reported as "try again in 10s" rather than "try again". ## Depends on The platform-side endpoint `GET /cli/applications/{slug}/services/{service}/logs` — [Someones.Computer#155](https://git.grey.ooo/Grey.ooo/Someones.Computer/pulls/155). Against a platform without it, `sc logs` says *"…cannot stream logs — `sc logs` needs a newer platform"* rather than "not found", because a 404 with no `error` key is a missing route, not a missing application. There's a test for that too. ## Verification - `go vet ./...`, `go test ./...` — clean. - Cross-compiled linux/amd64, darwin/arm64, windows/amd64 (the CI matrix's shape). - **Run against a live platform**, not only stubs: the transcript above is a real Postgres service, and the 409 (no such service), 404 (no such application) and missing-token paths were checked through the built binary. 19 new tests — 11 on the client (framing, ping, escaping, cancellation, the timeout, each error shape) and 8 on the command (stream separation, the flags, and each error turned into a sentence someone can act on).
Add sc logs — follow a running service's output
All checks were successful
CI / build (pull_request) Successful in 3m31s
CI / build (push) Successful in 1m53s
6e743f0dfa
The platform serves the tail as newline-delimited JSON on the bearer-token
firewall; this reads it. `sc logs <service>` streams one service's stdout and
stderr as it happens, with --tail N of history first (200 by default, 0 to
start from now).

Log lines go to stdout and everything else — which application on which
endpoint, why the stream ended — to stderr, so `sc logs web > web.log`
collects the log and nothing else.

The application resolves exactly as `deploy` resolves it (--app, then .sc.yml,
then the directory name), so the two cannot mean different applications in one
checkout.

Three things the framing buys, which a raw text stream could not:

  - a keep-alive is recognisable as one and is swallowed rather than printed;
  - the stream ending on purpose is told apart from the connection dropping,
    and only the first carries a reason;
  - a frame from a newer platform that this client cannot parse is skipped
    rather than ending the tail, so the lines either side still arrive.

The streaming request deliberately does not use the shared http.Client: its
five-minute ceiling is exactly wrong for a tail, whose job is to stay open and
say nothing for long stretches. The deadline that matters is the context's.

main now installs a signal handler and runs the command with that context.
Ctrl-C is how a person stops watching a log, so cancellation is reported as a
quiet end rather than an error — and every other command now gets a request
that is actually abandoned instead of one the platform answers into a dead
socket.

APIError carries Retry-After, so a full log-stream pool can be reported as
"try again in 10s" rather than "try again".

Verified against a live platform: streamed a real Postgres service, and
checked the 409 (no such service), 404 (no such application) and missing-token
paths through the built binary.

Requires the platform-side endpoint —
GET /cli/applications/{slug}/services/{service}/logs.
Sign in to join this conversation.
No reviewers
No labels
in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/someones.computer_agent!12
No description provided.