distinguish stdout and stderr in up logs

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2022-12-13 08:51:55 +01:00 committed by Nicolas De loof
parent 3ee2ab87bb
commit 0368f19030
12 changed files with 105 additions and 35 deletions

View File

@ -67,7 +67,7 @@ func runLogs(ctx context.Context, backend api.Service, opts logsOptions, service
if err != nil { if err != nil {
return err 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{ return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project, Project: project,
Services: services, Services: services,

View File

@ -176,7 +176,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions
var consumer api.LogConsumer var consumer api.LogConsumer
if !upOptions.Detach { 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 attachTo := services

View File

@ -32,18 +32,20 @@ type logConsumer struct {
ctx context.Context ctx context.Context
presenters sync.Map // map[string]*presenter presenters sync.Map // map[string]*presenter
width int width int
writer io.Writer stdout io.Writer
stderr io.Writer
color bool color bool
prefix bool prefix bool
} }
// NewLogConsumer creates a new LogConsumer // 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{ return &logConsumer{
ctx: ctx, ctx: ctx,
presenters: sync.Map{}, presenters: sync.Map{},
width: 0, width: 0,
writer: w, stdout: stdout,
stderr: stderr,
color: color, color: color,
prefix: prefix, prefix: prefix,
} }
@ -83,20 +85,29 @@ func (l *logConsumer) getPresenter(container string) *presenter {
} }
// Log formats a log message as received from name/container // 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 { if l.ctx.Err() != nil {
return return
} }
p := l.getPresenter(container) p := l.getPresenter(container)
for _, line := range strings.Split(message, "\n") { 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) { func (l *logConsumer) Status(container, msg string) {
p := l.getPresenter(container) p := l.getPresenter(container)
s := p.colors(fmt.Sprintf("%s %s\n", container, msg)) 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() { func (l *logConsumer) computeWidth() {

View File

@ -437,7 +437,8 @@ type Stack struct {
// LogConsumer is a callback to process log messages from services // LogConsumer is a callback to process log messages from services
type LogConsumer interface { type LogConsumer interface {
Log(containerName, service, message string) Log(containerName, message string)
Err(containerName, message string)
Status(container, msg string) Status(container, msg string)
Register(container string) Register(container string)
} }
@ -461,8 +462,10 @@ type ContainerEvent struct {
} }
const ( 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 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 is a ContainerEvent of type attach. First event sent about a container
ContainerEventAttach ContainerEventAttach
// ContainerEventStopped is a ContainerEvent of type stopped. // ContainerEventStopped is a ContainerEvent of type stopped.

View File

@ -69,7 +69,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
Service: serviceName, Service: serviceName,
}) })
w := utils.GetWriter(func(line string) { wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventLog, Type: api.ContainerEventLog,
Container: containerName, Container: containerName,
@ -77,13 +77,21 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
Line: line, 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) inspect, err := s.dockerCli.Client().ContainerInspect(ctx, container.ID)
if err != nil { if err != nil {
return err 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 return err
} }

View File

@ -99,7 +99,6 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons
return err return err
} }
service := c.Labels[api.ServiceLabel]
r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, types.ContainerLogsOptions{ r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
@ -116,7 +115,7 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons
name := getContainerNameWithoutProject(c) name := getContainerNameWithoutProject(c)
w := utils.GetWriter(func(line string) { w := utils.GetWriter(func(line string) {
consumer.Log(name, service, line) consumer.Log(name, line)
}) })
if cnt.Config.Tty { if cnt.Config.Tty {
_, err = io.Copy(w, r) _, err = io.Copy(w, r)

View File

@ -98,7 +98,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
require.Equal( require.Equal(
t, t,
[]string{"hello stdout", "hello stderr"}, []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) err := tested.Logs(ctx, name, consumer, opts)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("serviceA", "c1")) require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("c1"))
require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("serviceA", "c2")) require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("c2"))
require.Empty(t, consumer.LogsForContainer("serviceB", "c3")) require.Empty(t, consumer.LogsForContainer("c3"))
require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("serviceC", "c4")) require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("c4"))
} }
type testLogConsumer struct { type testLogConsumer struct {
mu sync.Mutex mu sync.Mutex
// logs is keyed by service, then container; values are log lines // logs is keyed container; values are log lines
logs map[string]map[string][]string logs map[string][]string
} }
func (l *testLogConsumer) Log(containerName, service, message string) { func (l *testLogConsumer) Log(containerName, message string) {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
if l.logs == nil { 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[containerName] = append(l.logs[containerName], message)
l.logs[service] = make(map[string][]string)
} }
l.logs[service][containerName] = append(l.logs[service][containerName], message)
func (l *testLogConsumer) Err(containerName, message string) {
l.Log(containerName, message)
} }
func (l *testLogConsumer) Status(containerName, msg string) {} func (l *testLogConsumer) Status(containerName, msg string) {}
func (l *testLogConsumer) Register(containerName 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() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
return l.logs[svc][containerName] return l.logs[containerName]
} }

View File

@ -118,7 +118,11 @@ func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string
} }
case api.ContainerEventLog: case api.ContainerEventLog:
if !aborting { 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)
} }
} }
} }

View File

@ -66,3 +66,13 @@ func TestPortRange(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans") 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")
}

View File

@ -0,0 +1,6 @@
services:
stderr:
image: alpine
command: /bin/ash /log_to_stderr.sh
volumes:
- ./log_to_stderr.sh:/log_to_stderr.sh

View File

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

View File

@ -416,16 +416,28 @@ func (m *MockLogConsumer) EXPECT() *MockLogConsumerMockRecorder {
return m.recorder return m.recorder
} }
// Log mocks base method. // Err mocks base method.
func (m *MockLogConsumer) Log(containerName, service, message string) { func (m *MockLogConsumer) Err(containerName, message string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Log", containerName, service, message) 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, message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Log", containerName, message)
} }
// Log indicates an expected call of Log. // 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() 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. // Register mocks base method.