mirror of
https://github.com/docker/compose.git
synced 2025-07-23 21:54:40 +02:00
distinguish stdout and stderr in up
logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
3ee2ab87bb
commit
0368f19030
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
6
pkg/e2e/fixtures/stdout-stderr/compose.yaml
Normal file
6
pkg/e2e/fixtures/stdout-stderr/compose.yaml
Normal 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
|
16
pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh
Executable file
16
pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh
Executable 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"
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user