Skip to content

Commit b7f689d

Browse files
authored
fix(podman): use attach API for real-time container log streaming (#137)
Replace the logs API with the attach API for streaming container output. The logs API reads from conmon's log file, adding 100-250ms of latency. Attach connects directly to the container's stdio via a WebSocket-upgraded connection, achieving sub-millisecond lag — matching Docker's behavior.
1 parent a5af98a commit b7f689d

1 file changed

Lines changed: 17 additions & 55 deletions

File tree

pkg/podman/podman.go

Lines changed: 17 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -337,26 +337,16 @@ func (m *manager) RunInitContainer(
337337
}
338338

339339
// StreamLogs streams container logs to the provided writers.
340+
// Note: Podman's REST API delivers logs in bursts with higher latency
341+
// than Docker's multiplexed binary stream. This is a known limitation.
340342
func (m *manager) StreamLogs(
341343
ctx context.Context,
342344
containerID string,
343345
stdout, stderr io.Writer,
344346
) error {
345-
// Unlike Docker's ContainerLogs (which waits for a "created" container
346-
// to start producing output), Podman's Logs API returns immediately
347-
// with EOF when the container hasn't started yet. Poll until the
348-
// container is running so we don't silently lose all log output.
349-
if err := m.waitForRunning(ctx, containerID); err != nil {
350-
return fmt.Errorf("waiting for container to start: %w", err)
351-
}
352-
353-
follow := true
354-
showStdout := true
355-
showStderr := true
356-
357347
// Derive a context from m.conn (carries Podman connection info) that
358348
// also cancels when the caller's ctx is cancelled. This ensures that
359-
// follow-mode log streaming terminates when the caller cancels (e.g.,
349+
// the attach connection terminates when the caller cancels (e.g.,
360350
// after a container checkpoint).
361351
logConn, cancel := context.WithCancel(m.conn)
362352
defer cancel()
@@ -369,50 +359,22 @@ func (m *manager) StreamLogs(
369359
}
370360
}()
371361

372-
stdoutCh := make(chan string, 100)
373-
stderrCh := make(chan string, 100)
374-
375-
var wg sync.WaitGroup
376-
377-
wg.Add(1)
378-
379-
go func() {
380-
defer wg.Done()
381-
382-
for line := range stdoutCh {
383-
if stdout != nil {
384-
_, _ = io.WriteString(stdout, line+"\n")
385-
}
386-
}
387-
}()
388-
389-
wg.Add(1)
390-
391-
go func() {
392-
defer wg.Done()
393-
394-
for line := range stderrCh {
395-
if stderr != nil {
396-
_, _ = io.WriteString(stderr, line+"\n")
397-
}
362+
// Use the attach API instead of logs. The logs API reads from the
363+
// container's log file (written by conmon) which adds 100-250ms of
364+
// latency. Attach connects directly to the container's stdio streams
365+
// via a WebSocket-upgraded connection, giving us real-time output —
366+
// the same low-latency behavior as Docker's ContainerLogs.
367+
//
368+
// Unlike the logs API, attach works on "created" containers (it blocks
369+
// until the container starts), so we don't need waitForRunning.
370+
err := containers.Attach(logConn, containerID, nil, stdout, stderr, nil, nil)
371+
if err != nil {
372+
// Context cancellation is expected during cleanup.
373+
if ctx.Err() != nil {
374+
return nil
398375
}
399-
}()
400-
401-
err := containers.Logs(logConn, containerID, &containers.LogOptions{
402-
Follow: &follow,
403-
Stdout: &showStdout,
404-
Stderr: &showStderr,
405-
}, stdoutCh, stderrCh)
406-
407-
// Podman's Logs does not close the channels on return. Close them so
408-
// the reader goroutines exit their range loops.
409-
close(stdoutCh)
410-
close(stderrCh)
411376

412-
wg.Wait()
413-
414-
if err != nil {
415-
return fmt.Errorf("streaming logs: %w", err)
377+
return fmt.Errorf("attaching to container: %w", err)
416378
}
417379

418380
return nil

0 commit comments

Comments
 (0)