Skip to content

Commit 7f916ae

Browse files
fix: strip headers from logs using log stream specification (#3226)
* modify log consumer test to show how stripping logs sometimes fails bug introduced by #454 * fix: implement the docker log stream specification to strip headers from logs * chore: lint * chore: use require --------- Co-authored-by: Manuel de la Peña <[email protected]> Co-authored-by: Manuel de la Peña <[email protected]>
1 parent 295ba91 commit 7f916ae

File tree

3 files changed

+54
-29
lines changed

3 files changed

+54
-29
lines changed

docker.go

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bufio"
66
"context"
77
"encoding/base64"
8+
"encoding/binary"
89
"encoding/json"
910
"errors"
1011
"fmt"
@@ -367,8 +368,6 @@ func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*container.I
367368
// Logs will fetch both STDOUT and STDERR from the current container. Returns a
368369
// ReadCloser and leaves it up to the caller to extract what it wants.
369370
func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
370-
const streamHeaderSize = 8
371-
372371
options := container.LogsOptions{
373372
ShowStdout: true,
374373
ShowStderr: true,
@@ -380,42 +379,43 @@ func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
380379
}
381380
defer c.provider.Close()
382381

382+
resp, err := c.Inspect(ctx)
383+
if err != nil {
384+
return nil, err
385+
}
386+
387+
if resp.Config.Tty {
388+
return rc, nil
389+
}
390+
391+
return c.parseMultiplexedLogs(rc), nil
392+
}
393+
394+
// parseMultiplexedLogs handles the multiplexed log format used when TTY is disabled
395+
func (c *DockerContainer) parseMultiplexedLogs(rc io.ReadCloser) io.ReadCloser {
396+
const streamHeaderSize = 8
397+
383398
pr, pw := io.Pipe()
384399
r := bufio.NewReader(rc)
385400

386401
go func() {
387-
lineStarted := true
388-
for err == nil {
389-
line, isPrefix, err := r.ReadLine()
390-
391-
if lineStarted && len(line) >= streamHeaderSize {
392-
line = line[streamHeaderSize:] // trim stream header
393-
lineStarted = false
394-
}
395-
if !isPrefix {
396-
lineStarted = true
397-
}
398-
399-
_, errW := pw.Write(line)
400-
if errW != nil {
402+
header := make([]byte, streamHeaderSize)
403+
for {
404+
_, errH := io.ReadFull(r, header)
405+
if errH != nil {
406+
_ = pw.CloseWithError(errH)
401407
return
402408
}
403409

404-
if !isPrefix {
405-
_, errW := pw.Write([]byte("\n"))
406-
if errW != nil {
407-
return
408-
}
409-
}
410-
411-
if err != nil {
412-
_ = pw.CloseWithError(err)
410+
frameSize := binary.BigEndian.Uint32(header[4:])
411+
if _, err := io.CopyN(pw, r, int64(frameSize)); err != nil {
412+
pw.CloseWithError(err)
413413
return
414414
}
415415
}
416416
}()
417417

418-
return pr, nil
418+
return pr
419419
}
420420

421421
// Deprecated: use the ContainerRequest.LogConsumerConfig field instead.

from_dockerfile_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func TestBuildImageFromDockerfile_Target(t *testing.T) {
159159

160160
logs, err := io.ReadAll(r)
161161
require.NoError(t, err)
162-
require.Equal(t, fmt.Sprintf("target%d\n\n", i), string(logs))
162+
require.Equal(t, fmt.Sprintf("target%d\n", i), string(logs))
163163
}
164164
}
165165

logconsumer_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func TestContainerLogsShouldBeWithoutStreamHeader(t *testing.T) {
363363
ctx := context.Background()
364364
req := ContainerRequest{
365365
Image: "alpine:latest",
366-
Cmd: []string{"sh", "-c", "id -u"},
366+
Cmd: []string{"sh", "-c", "echo 'abcdefghi' && echo 'foo'"},
367367
WaitingFor: wait.ForExit(),
368368
}
369369
ctr, err := GenericContainer(ctx, GenericContainerRequest{
@@ -378,7 +378,32 @@ func TestContainerLogsShouldBeWithoutStreamHeader(t *testing.T) {
378378
defer r.Close()
379379
b, err := io.ReadAll(r)
380380
require.NoError(t, err)
381-
assert.Equal(t, "0", strings.TrimSpace(string(b)))
381+
require.Equal(t, "abcdefghi\nfoo", strings.TrimSpace(string(b)))
382+
}
383+
384+
func TestContainerLogsTty(t *testing.T) {
385+
ctx := context.Background()
386+
req := ContainerRequest{
387+
Image: "alpine:latest",
388+
Cmd: []string{"sh", "-c", "echo 'abcdefghi' && echo 'foo'"},
389+
ConfigModifier: func(ctr *container.Config) {
390+
ctr.Tty = true
391+
},
392+
WaitingFor: wait.ForExit(),
393+
}
394+
ctr, err := GenericContainer(ctx, GenericContainerRequest{
395+
ContainerRequest: req,
396+
Started: true,
397+
})
398+
CleanupContainer(t, ctr)
399+
require.NoError(t, err)
400+
401+
r, err := ctr.Logs(ctx)
402+
require.NoError(t, err)
403+
defer r.Close()
404+
b, err := io.ReadAll(r)
405+
require.NoError(t, err)
406+
require.Equal(t, "abcdefghi\r\nfoo", strings.TrimSpace(string(b)))
382407
}
383408

384409
func TestContainerLogsEnableAtStart(t *testing.T) {

0 commit comments

Comments
 (0)