فهرست منبع

distinguish stdout and stderr in `up` logs

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 سال پیش
والد
کامیت
0368f19030

+ 1 - 1
cmd/compose/logs.go

@@ -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,

+ 1 - 1
cmd/compose/up.go

@@ -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

+ 17 - 6
cmd/formatter/logs.go

@@ -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,
 	}
@@ -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() {

+ 5 - 2
pkg/api/api.go

@@ -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)
 }
@@ -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.

+ 10 - 2
pkg/compose/attach.go

@@ -69,7 +69,7 @@ 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,
@@ -77,13 +77,21 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
 			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
 }
 

+ 1 - 2
pkg/compose/logs.go

@@ -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,
@@ -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)
+		consumer.Log(name, line)
 	})
 	if cnt.Config.Tty {
 		_, err = io.Copy(w, r)

+ 16 - 15
pkg/compose/logs_test.go

@@ -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"),
 	)
 }
 
@@ -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]
 }

+ 5 - 1
pkg/compose/printer.go

@@ -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)
 				}
 			}
 		}

+ 10 - 0
pkg/e2e/compose_up_test.go

@@ -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 - 0
pkg/e2e/fixtures/stdout-stderr/compose.yaml

@@ -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 - 0
pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh

@@ -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"

+ 16 - 4
pkg/mocks/mock_docker_compose_api.go

@@ -416,16 +416,28 @@ func (m *MockLogConsumer) EXPECT() *MockLogConsumerMockRecorder {
 	return m.recorder
 }
 
+// Err mocks base method.
+func (m *MockLogConsumer) Err(containerName, message string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Err", containerName, message)
+}
+
+// Err indicates an expected call of Err.
+func (mr *MockLogConsumerMockRecorder) Err(containerName, message interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockLogConsumer)(nil).Err), containerName, message)
+}
+
 // Log mocks base method.
-func (m *MockLogConsumer) Log(containerName, service, message string) {
+func (m *MockLogConsumer) Log(containerName, message string) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Log", containerName, service, message)
+	m.ctrl.Call(m, "Log", containerName, message)
 }
 
 // Log indicates an expected call of Log.
-func (mr *MockLogConsumerMockRecorder) Log(containerName, service, message interface{}) *gomock.Call {
+func (mr *MockLogConsumerMockRecorder) Log(containerName, message interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogConsumer)(nil).Log), containerName, service, message)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogConsumer)(nil).Log), containerName, message)
 }
 
 // Register mocks base method.