Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/compose/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func runLogs(ctx context.Context, backend api.Service, opts logsOptions, service
if err != nil {
return err
}
consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
consumer := formatter.NewLogConsumer(ctx, os.Stdout, os.Stderr, !opts.noColor, !opts.noPrefix)
return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project,
Services: services,
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions

var consumer api.LogConsumer
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, os.Stdout, !upOptions.noColor, !upOptions.noPrefix)
consumer = formatter.NewLogConsumer(ctx, os.Stdout, os.Stderr, !upOptions.noColor, !upOptions.noPrefix)
}

attachTo := services
Expand Down
23 changes: 17 additions & 6 deletions cmd/formatter/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,20 @@ type logConsumer struct {
ctx context.Context
presenters sync.Map // map[string]*presenter
width int
writer io.Writer
stdout io.Writer
stderr io.Writer
color bool
prefix bool
}

// NewLogConsumer creates a new LogConsumer
func NewLogConsumer(ctx context.Context, w io.Writer, color bool, prefix bool) api.LogConsumer {
func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color bool, prefix bool) api.LogConsumer {
return &logConsumer{
ctx: ctx,
presenters: sync.Map{},
width: 0,
writer: w,
stdout: stdout,
stderr: stderr,
color: color,
prefix: prefix,
}
Expand Down Expand Up @@ -83,20 +85,29 @@ func (l *logConsumer) getPresenter(container string) *presenter {
}

// Log formats a log message as received from name/container
func (l *logConsumer) Log(container, service, message string) {
func (l *logConsumer) Log(container, message string) {
l.write(l.stdout, container, message)
}

// Log formats a log message as received from name/container
func (l *logConsumer) Err(container, message string) {
l.write(l.stderr, container, message)
}

func (l *logConsumer) write(w io.Writer, container, message string) {
if l.ctx.Err() != nil {
return
}
p := l.getPresenter(container)
for _, line := range strings.Split(message, "\n") {
fmt.Fprintf(l.writer, "%s%s\n", p.prefix, line)
fmt.Fprintf(w, "%s%s\n", p.prefix, line)
}
}

func (l *logConsumer) Status(container, msg string) {
p := l.getPresenter(container)
s := p.colors(fmt.Sprintf("%s %s\n", container, msg))
l.writer.Write([]byte(s)) //nolint:errcheck
l.stdout.Write([]byte(s)) //nolint:errcheck
}

func (l *logConsumer) computeWidth() {
Expand Down
7 changes: 5 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ type Stack struct {

// LogConsumer is a callback to process log messages from services
type LogConsumer interface {
Log(containerName, service, message string)
Log(containerName, message string)
Err(containerName, message string)
Status(container, msg string)
Register(container string)
}
Expand All @@ -461,8 +462,10 @@ type ContainerEvent struct {
}

const (
// ContainerEventLog is a ContainerEvent of type log. Line is set
// ContainerEventLog is a ContainerEvent of type log on stdout. Line is set
ContainerEventLog = iota
// ContainerEventErr is a ContainerEvent of type log on stderr. Line is set
ContainerEventErr
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
ContainerEventAttach
// ContainerEventStopped is a ContainerEvent of type stopped.
Expand Down
12 changes: 10 additions & 2 deletions pkg/compose/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,29 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
Service: serviceName,
})

w := utils.GetWriter(func(line string) {
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventLog,
Container: containerName,
Service: serviceName,
Line: line,
})
})
wErr := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventErr,
Container: containerName,
Service: serviceName,
Line: line,
})
})

inspect, err := s.dockerCli.Client().ContainerInspect(ctx, container.ID)
if err != nil {
return err
}

_, _, err = s.attachContainerStreams(ctx, container.ID, inspect.Config.Tty, nil, w, w)
_, _, err = s.attachContainerStreams(ctx, container.ID, inspect.Config.Tty, nil, wOut, wErr)
return err
}

Expand Down
3 changes: 1 addition & 2 deletions pkg/compose/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons
return err
}

service := c.Labels[api.ServiceLabel]
r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Expand All @@ -116,7 +115,7 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons

name := getContainerNameWithoutProject(c)
w := utils.GetWriter(func(line string) {
consumer.Log(name, service, line)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really want to remove the reference to the service name in the logs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's actually unused (this is legacy from early ECS-CLI-plugin 😅

consumer.Log(name, line)
})
if cnt.Config.Tty {
_, err = io.Copy(w, r)
Expand Down
31 changes: 16 additions & 15 deletions pkg/compose/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
require.Equal(
t,
[]string{"hello stdout", "hello stderr"},
consumer.LogsForContainer("service", "c"),
consumer.LogsForContainer("c"),
)
}

Expand Down Expand Up @@ -169,36 +169,37 @@ func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
err := tested.Logs(ctx, name, consumer, opts)
require.NoError(t, err)

require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("serviceA", "c1"))
require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("serviceA", "c2"))
require.Empty(t, consumer.LogsForContainer("serviceB", "c3"))
require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("serviceC", "c4"))
require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("c1"))
require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("c2"))
require.Empty(t, consumer.LogsForContainer("c3"))
require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("c4"))
}

type testLogConsumer struct {
mu sync.Mutex
// logs is keyed by service, then container; values are log lines
logs map[string]map[string][]string
// logs is keyed container; values are log lines
logs map[string][]string
}

func (l *testLogConsumer) Log(containerName, service, message string) {
func (l *testLogConsumer) Log(containerName, message string) {
l.mu.Lock()
defer l.mu.Unlock()
if l.logs == nil {
l.logs = make(map[string]map[string][]string)
l.logs = make(map[string][]string)
}
if l.logs[service] == nil {
l.logs[service] = make(map[string][]string)
}
l.logs[service][containerName] = append(l.logs[service][containerName], message)
l.logs[containerName] = append(l.logs[containerName], message)
}

func (l *testLogConsumer) Err(containerName, message string) {
l.Log(containerName, message)
}

func (l *testLogConsumer) Status(containerName, msg string) {}

func (l *testLogConsumer) Register(containerName string) {}

func (l *testLogConsumer) LogsForContainer(svc string, containerName string) []string {
func (l *testLogConsumer) LogsForContainer(containerName string) []string {
l.mu.Lock()
defer l.mu.Unlock()
return l.logs[svc][containerName]
return l.logs[containerName]
}
6 changes: 5 additions & 1 deletion pkg/compose/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string
}
case api.ContainerEventLog:
if !aborting {
p.consumer.Log(container, event.Service, event.Line)
p.consumer.Log(container, event.Line)
}
case api.ContainerEventErr:
if !aborting {
p.consumer.Err(container, event.Line)
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/e2e/compose_up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ func TestPortRange(t *testing.T) {

c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}

func TestStdoutStderr(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-stdout-stderr"

res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/stdout-stderr/compose.yaml", "--project-name", projectName, "up")
res.Assert(t, icmd.Expected{Out: "log to stdout", Err: "log to stderr"})

c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
6 changes: 6 additions & 0 deletions pkg/e2e/fixtures/stdout-stderr/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
stderr:
image: alpine
command: /bin/ash /log_to_stderr.sh
volumes:
- ./log_to_stderr.sh:/log_to_stderr.sh
16 changes: 16 additions & 0 deletions pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2020 Docker Compose CLI authors

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

>&2 echo "log to stderr"
echo "log to stdout"
20 changes: 16 additions & 4 deletions pkg/mocks/mock_docker_compose_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.