Skip to content

Add lstk logs command#42

Merged
carole-lavillonniere merged 1 commit intomainfrom
carole/drg-367
Feb 19, 2026
Merged

Add lstk logs command#42
carole-lavillonniere merged 1 commit intomainfrom
carole/drg-367

Conversation

@carole-lavillonniere
Copy link
Collaborator

lstk logs streams container logs to the terminal until the user cancels with Ctrl+C.

@carole-lavillonniere
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds a new logs CLI command and end-to-end log streaming: runtime StreamLogs API, Docker implementation, container-level orchestration to scan and emit per-line log events, output event/sink support, and integration tests validating streaming and error cases.

Changes

Cohort / File(s) Summary
CLI Command
cmd/logs.go
New logs command: initializes Docker runtime and calls container.Logs with a plain stdout sink.
Container orchestration
internal/container/logs.go
New Logs(ctx, rt, sink) that selects first configured container, streams runtime logs into an in-memory pipe, scans lines, and emits ContainerLogLineEvent per line; propagates streaming errors.
Runtime interface & Docker
internal/runtime/runtime.go, internal/runtime/docker.go
Adds StreamLogs(ctx, containerID, out io.Writer) error to Runtime and implements it in DockerRuntime using Docker GetLogs + stdcopy demultiplexing; handles NotFound and context cancellation.
Output events & formatting
internal/output/events.go, internal/output/format.go
Adds ContainerLogLineEvent and helper EmitContainerLogLine; FormatEventLine now renders log lines.
Output tests
internal/output/plain_sink_test.go
New test verifying ContainerLogLineEvent is written to plain sink with trailing newline.
Integration tests
test/integration/logs_test.go
Adds tests: error when emulator not running; streaming test that starts a container, runs logs command, injects a marker via docker exec, and asserts marker appears in streamed output.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Cmd as logs Command
    participant Container as container.Logs
    participant Runtime as DockerRuntime
    participant Docker as Docker API
    participant Sink as Output Sink

    User->>Cmd: Run "logs"
    Cmd->>Runtime: runtime.NewDockerRuntime()
    Cmd->>Container: Logs(ctx, runtime, sink)
    Container->>Container: select first container
    Container->>Runtime: StreamLogs(ctx, containerID, pipeWriter)
    activate Runtime
    Runtime->>Docker: GetLogs(Follow, Stdout, Stderr, Tail=all)
    Docker-->>Runtime: log stream reader
    Runtime->>pipeWriter: stdcopy demux -> writer
    deactivate Runtime
    Container->>Container: scan pipeReader line-by-line
    loop per line
        Container->>Sink: EmitContainerLogLine(line)
        Sink-->>User: writes line to stdout
    end
    Container-->>Cmd: return nil or error
    Cmd-->>User: exit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'Add lstk logs command' clearly and concisely summarizes the main change—adding a new logs command to the CLI tool.
Description check ✅ Passed The description 'lstk logs streams container logs to the terminal until the user cancels with Ctrl+C' directly relates to the changeset and accurately describes the functionality being added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch carole/drg-367

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
internal/output/plain_sink_test.go (1)

143-150: Test looks good, but consider updating TestPlainSink_UsesFormatterParity.

The new test correctly verifies ContainerLogLineEvent emission. However, TestPlainSink_UsesFormatterParity (lines 183-188) tests formatter/sink parity for all event types but doesn't include ContainerLogLineEvent in its events slice. Based on learnings: "When adding a new event type, update all of: internal/output/events.go (event type + union + emit helper), internal/output/format.go (line formatting fallback), and tests in internal/output/*_test.go for formatter/sink behavior parity."

♻️ Add ContainerLogLineEvent to parity test
 	events := []any{
 		LogEvent{Message: "hello"},
 		WarningEvent{Message: "careful"},
 		ContainerStatusEvent{Phase: "starting", Container: "localstack"},
 		ProgressEvent{LayerID: "abc", Status: "Downloading", Current: 1, Total: 2},
+		ContainerLogLineEvent{Line: "2024-01-01 log line"},
 	}

And add a case in the switch:

 		case ProgressEvent:
 			Emit(sink, e)
+		case ContainerLogLineEvent:
+			Emit(sink, e)
 		default:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/output/plain_sink_test.go` around lines 143 - 150, Update the parity
test TestPlainSink_UsesFormatterParity to include the new ContainerLogLineEvent
so formatter/sink parity covers it: add ContainerLogLineEvent to the events
slice used in that test and add a corresponding branch in the switch that
constructs the expected formatted output for ContainerLogLineEvent (matching
NewPlainSink/Emit behavior and the fallback in FormatLine), ensuring the test
asserts the same formatted string as the sink emits.
internal/runtime/docker.go (1)

163-165: Consider making the error message more generic or parameterized.

The error message "emulator is not running" uses LocalStack-specific terminology, but StreamLogs is a generic runtime method that could stream logs for any container. Consider making this more generic or including the container ID.

♻️ Suggested improvement
 	if errdefs.IsNotFound(err) {
-		return fmt.Errorf("emulator is not running. Start LocalStack with `lstk`")
+		return fmt.Errorf("container %s is not running", containerID)
 	}

Alternatively, keep the user-friendly message but move it to the container.Logs layer where the domain context is clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runtime/docker.go` around lines 163 - 165, The error returned in
internal/runtime/docker.go inside the errdefs.IsNotFound(err) branch is
LocalStack-specific; update the message in the StreamLogs path to be generic and
include the container identifier (e.g., use the containerID or containerName
variable) so it reads like "container <ID> is not running" (or parameterize the
message), and optionally move a user-friendly LocalStack-specific phrasing into
the container.Logs layer if domain context is required; change the fmt.Errorf
call in the errdefs.IsNotFound(err) block of StreamLogs accordingly.
internal/container/logs.go (1)

34-41: Consider closing the pipe reader on early exit to unblock the streaming goroutine.

If scanner.Err() returns an error (line 38-39) before the streaming goroutine completes, the function returns the scanner error but then blocks on <-errCh (line 41). The goroutine may remain blocked on pw.Write() until the context is cancelled. Closing pr would cause the write to fail immediately, unblocking the goroutine.

♻️ Proposed fix
 	scanner := bufio.NewScanner(pr)
 	for scanner.Scan() {
 		output.EmitContainerLogLine(sink, scanner.Text())
 	}
 	if err := scanner.Err(); err != nil && ctx.Err() == nil {
+		pr.Close() // unblock the streaming goroutine
 		return err
 	}
 	return <-errCh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/container/logs.go` around lines 34 - 41, When scanner.Scan() stops
with an error, close the pipe reader `pr` to unblock the streaming goroutine
before returning the scanner error; specifically, in the block that checks `if
err := scanner.Err(); err != nil && ctx.Err() == nil { ... }` call `pr.Close()`
(or `pr.CloseWithError(err)` if available) and then return the scanner error, so
the goroutine blocked on `pw.Write()` wakes and writes to `errCh`; reference
`scanner`, `pr`, `pw.Write()`, `errCh`, and `output.EmitContainerLogLine` to
locate the surrounding logic.
test/integration/logs_test.go (1)

51-52: Fixed sleep may cause flaky tests.

The 500ms sleep to wait for the logs connection could be insufficient on slower CI environments or excessive on fast ones. Consider a more robust synchronization approach, such as waiting for initial output or using a ready signal.

That said, for an integration test of this nature, a short sleep is often pragmatic. If flakiness is observed, consider increasing the timeout or adding retry logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/logs_test.go` around lines 51 - 52, Replace the fixed sleep
(time.Sleep(500 * time.Millisecond)) used to wait for "lstk" logs with a
deterministic wait: poll or block on a ready condition (e.g., an exported
channel, a connection-accept event, or a small retry loop checking for initial
output) until the logging endpoint is reachable or a short timeout elapses;
update the test in logs_test.go to use that readiness check instead of the
hardcoded sleep so CI flakiness is avoided while still failing quickly on
timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/container/logs.go`:
- Around line 34-41: When scanner.Scan() stops with an error, close the pipe
reader `pr` to unblock the streaming goroutine before returning the scanner
error; specifically, in the block that checks `if err := scanner.Err(); err !=
nil && ctx.Err() == nil { ... }` call `pr.Close()` (or `pr.CloseWithError(err)`
if available) and then return the scanner error, so the goroutine blocked on
`pw.Write()` wakes and writes to `errCh`; reference `scanner`, `pr`,
`pw.Write()`, `errCh`, and `output.EmitContainerLogLine` to locate the
surrounding logic.

In `@internal/output/plain_sink_test.go`:
- Around line 143-150: Update the parity test TestPlainSink_UsesFormatterParity
to include the new ContainerLogLineEvent so formatter/sink parity covers it: add
ContainerLogLineEvent to the events slice used in that test and add a
corresponding branch in the switch that constructs the expected formatted output
for ContainerLogLineEvent (matching NewPlainSink/Emit behavior and the fallback
in FormatLine), ensuring the test asserts the same formatted string as the sink
emits.

In `@internal/runtime/docker.go`:
- Around line 163-165: The error returned in internal/runtime/docker.go inside
the errdefs.IsNotFound(err) branch is LocalStack-specific; update the message in
the StreamLogs path to be generic and include the container identifier (e.g.,
use the containerID or containerName variable) so it reads like "container <ID>
is not running" (or parameterize the message), and optionally move a
user-friendly LocalStack-specific phrasing into the container.Logs layer if
domain context is required; change the fmt.Errorf call in the
errdefs.IsNotFound(err) block of StreamLogs accordingly.

In `@test/integration/logs_test.go`:
- Around line 51-52: Replace the fixed sleep (time.Sleep(500 *
time.Millisecond)) used to wait for "lstk" logs with a deterministic wait: poll
or block on a ready condition (e.g., an exported channel, a connection-accept
event, or a small retry loop checking for initial output) until the logging
endpoint is reachable or a short timeout elapses; update the test in
logs_test.go to use that readiness check instead of the hardcoded sleep so CI
flakiness is avoided while still failing quickly on timeout.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/integration/logs_test.go (1)

51-52: Consider a more deterministic synchronization approach.

The 500ms sleep to wait for lstk logs to connect could be flaky on slower CI environments. Consider checking if the lstk logs process has produced initial output (e.g., scanning for any line before injecting the marker) or increasing the delay with a comment explaining the rationale.

This is a minor concern given the 30s overall timeout provides a safety net.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/logs_test.go` around lines 51 - 52, Replace the fixed
time.Sleep(500 * time.Millisecond) wait used to let the "lstk logs" process
connect with a deterministic wait that reads the process stdout/stderr and
blocks until the first log line appears (or a short timeout), then proceed to
inject the marker; update the code around the time.Sleep call and the logic that
launches "lstk logs" (search for the time.Sleep invocation and the process
launch for "lstk logs") to poll/read the process output stream instead of
sleeping to avoid flakes on slow CI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/runtime/docker.go`:
- Around line 162-166: Replace the incorrect containerd errdefs import with the
Docker client errdefs so Docker API errors are correctly detected: update the
import from "github.com/containerd/errdefs" to
"github.com/docker/docker/errdefs" and ensure calls like errdefs.IsNotFound(err)
in runtime/docker.go (the ContainerLogs handling), internal/container/stop.go,
and internal/container/start.go reference the docker/docker errdefs package so
the IsNotFound checks work as intended.

---

Nitpick comments:
In `@test/integration/logs_test.go`:
- Around line 51-52: Replace the fixed time.Sleep(500 * time.Millisecond) wait
used to let the "lstk logs" process connect with a deterministic wait that reads
the process stdout/stderr and blocks until the first log line appears (or a
short timeout), then proceed to inject the marker; update the code around the
time.Sleep call and the logic that launches "lstk logs" (search for the
time.Sleep invocation and the process launch for "lstk logs") to poll/read the
process output stream instead of sleeping to avoid flakes on slow CI.

Comment on lines +162 to +166
if err != nil {
if errdefs.IsNotFound(err) {
return fmt.Errorf("emulator is not running. Start LocalStack with `lstk`")
}
return fmt.Errorf("failed to stream logs for %s: %w", containerID, err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the imports in docker.go
echo "=== Checking imports in internal/runtime/docker.go ==="
head -30 internal/runtime/docker.go | cat -n

echo ""
echo "=== Checking all errdefs imports in the codebase ==="
rg "import.*errdefs" --type=go -B2 -A2

echo ""
echo "=== Checking how errdefs is used in the codebase ==="
rg "errdefs\." --type=go -B1 -A1

Repository: localstack/lstk

Length of output: 1835


🌐 Web query:

Docker go client github.com/docker/docker ContainerLogs error handling IsNotFound

💡 Result:

When cli.ContainerLogs(...) fails because the container doesn’t exist, the Docker Go client returns an error that should match Docker’s “not found” error-class. Check it with errdefs.IsNotFound(err) (it unwraps the causal chain, so it works even if the error is wrapped). (pkg.go.dev)

rc, err := cli.ContainerLogs(ctx, containerID, opts)
if err != nil {
    if errdefs.IsNotFound(err) {
        // container does not exist (HTTP 404 / "No such container")
        return
    }
    // other error
    return
}
defer rc.Close()

Imports:

import (
    "github.com/docker/docker/client"
    "github.com/docker/docker/errdefs"
)

errdefs.IsNotFound is the standard helper for this class of errors. (pkg.go.dev)

Citations:


Fix incorrect errdefs import for Docker client error handling.

The code imports github.com/containerd/errdefs (line 12) but should import github.com/docker/docker/errdefs when working with Docker client errors. The errdefs.IsNotFound() check at line 163 relies on the Docker-specific errdefs package to correctly identify "not found" errors from ContainerLogs(). Using containerd's errdefs may not properly detect Docker API errors, breaking the check for whether the container exists.

Change the import:

- "github.com/containerd/errdefs"
+ "github.com/docker/docker/errdefs"

This issue also affects internal/container/stop.go and internal/container/start.go, which use the same incorrect import pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runtime/docker.go` around lines 162 - 166, Replace the incorrect
containerd errdefs import with the Docker client errdefs so Docker API errors
are correctly detected: update the import from "github.com/containerd/errdefs"
to "github.com/docker/docker/errdefs" and ensure calls like
errdefs.IsNotFound(err) in runtime/docker.go (the ContainerLogs handling),
internal/container/stop.go, and internal/container/start.go reference the
docker/docker errdefs package so the IsNotFound checks work as intended.

Copy link
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! We can then do the TUI integration in a next iteration.

Also what the rabbit says seems legit, so might want to look into it :D Never would've caught that one

@carole-lavillonniere
Copy link
Collaborator Author

LGTM! We can then do the TUI integration in a next iteration.

Also what the rabbit says seems legit, so might want to look into it :D Never would've caught that one

Apparently it's deprecated in docker 🤷
https://github.com/moby/moby/blob/89c5e8fd66634b6128fc4c0e6f1236e2540e46e0/errdefs/is.go#L12
Claude: 1, Rabbit: 0

@carole-lavillonniere carole-lavillonniere merged commit f64ad5e into main Feb 19, 2026
8 checks passed
@carole-lavillonniere carole-lavillonniere deleted the carole/drg-367 branch February 19, 2026 14:43
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.

2 participants

Comments