Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit a69aa3d

Browse files
authored
Merge pull request #1285 from docker/predictable_colors
2 parents 54a7819 + 4e5734f commit a69aa3d

File tree

9 files changed

+138
-74
lines changed

9 files changed

+138
-74
lines changed

api/compose/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ type Stack struct {
184184
type LogConsumer interface {
185185
Log(service, container, message string)
186186
Status(service, container, msg string)
187+
Register(service string, source string)
187188
}
188189

189190
// ContainerEventListener is a callback to process ContainerEvent from services
@@ -201,6 +202,8 @@ type ContainerEvent struct {
201202
const (
202203
// ContainerEventLog is a ContainerEvent of type log. Line is set
203204
ContainerEventLog = iota
205+
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
206+
ContainerEventAttach
204207
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
205208
ContainerEventExit
206209
)

cli/cmd/compose/logs.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ import (
3131
type logsOptions struct {
3232
*projectOptions
3333
composeOptions
34-
follow bool
35-
tail string
34+
follow bool
35+
tail string
36+
noColor bool
37+
noPrefix bool
3638
}
3739

3840
func logsCommand(p *projectOptions, contextType string) *cobra.Command {
@@ -46,9 +48,13 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command {
4648
return runLogs(cmd.Context(), opts, args)
4749
},
4850
}
49-
logsCmd.Flags().BoolVar(&opts.follow, "follow", false, "Follow log output.")
51+
flags := logsCmd.Flags()
52+
flags.BoolVar(&opts.follow, "follow", false, "Follow log output.")
53+
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.")
54+
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")
55+
5056
if contextType == store.DefaultContextType {
51-
logsCmd.Flags().StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs for each container.")
57+
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs for each container.")
5258
}
5359
return logsCmd
5460
}
@@ -63,7 +69,7 @@ func runLogs(ctx context.Context, opts logsOptions, services []string) error {
6369
if err != nil {
6470
return err
6571
}
66-
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
72+
consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
6773
return c.ComposeService().Logs(ctx, projectName, consumer, compose.LogOptions{
6874
Services: services,
6975
Follow: opts.follow,

cli/cmd/compose/start.go

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828

2929
type startOptions struct {
3030
*projectOptions
31-
Detach bool
3231
}
3332

3433
func startCommand(p *projectOptions) *cobra.Command {
@@ -42,8 +41,6 @@ func startCommand(p *projectOptions) *cobra.Command {
4241
return runStart(cmd.Context(), opts, args)
4342
},
4443
}
45-
46-
startCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
4744
return startCmd
4845
}
4946

@@ -58,32 +55,8 @@ func runStart(ctx context.Context, opts startOptions, services []string) error {
5855
return err
5956
}
6057

61-
if opts.Detach {
62-
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
63-
return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
64-
})
65-
return err
66-
}
67-
68-
queue := make(chan compose.ContainerEvent)
69-
printer := printer{
70-
queue: queue,
71-
}
72-
err = c.ComposeService().Start(ctx, project, compose.StartOptions{
73-
Attach: func(event compose.ContainerEvent) {
74-
queue <- event
75-
},
76-
})
77-
if err != nil {
78-
return err
79-
}
80-
81-
_, err = printer.run(ctx, false, "", func() error {
82-
ctx := context.Background()
83-
_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
84-
return "", c.ComposeService().Stop(ctx, project)
85-
})
86-
return err
58+
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
59+
return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
8760
})
8861
return err
8962
}

cli/cmd/compose/up.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/compose-spec/compose-go/types"
3737
"github.com/sirupsen/logrus"
3838
"github.com/spf13/cobra"
39+
"golang.org/x/sync/errgroup"
3940
)
4041

4142
// composeOptions hold options common to `up` and `run` to run compose project
@@ -57,6 +58,8 @@ type upOptions struct {
5758
cascadeStop bool
5859
exitCodeFrom string
5960
scale []string
61+
noColor bool
62+
noPrefix bool
6063
}
6164

6265
func (o upOptions) recreateStrategy() string {
@@ -102,6 +105,8 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
102105
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
103106
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
104107
flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
108+
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.")
109+
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")
105110

106111
switch contextType {
107112
case store.AciContextType:
@@ -199,6 +204,16 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
199204
stopFunc() // nolint:errcheck
200205
}()
201206

207+
consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
208+
209+
var exitCode int
210+
eg, ctx := errgroup.WithContext(ctx)
211+
eg.Go(func() error {
212+
code, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, consumer, stopFunc)
213+
exitCode = code
214+
return err
215+
})
216+
202217
err = c.ComposeService().Start(ctx, project, compose.StartOptions{
203218
Attach: func(event compose.ContainerEvent) {
204219
queue <- event
@@ -208,7 +223,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
208223
return err
209224
}
210225

211-
exitCode, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, stopFunc)
226+
err = eg.Wait()
212227
if exitCode != 0 {
213228
return cmd.ExitCodeError{ExitCode: exitCode}
214229
}
@@ -298,27 +313,37 @@ type printer struct {
298313
queue chan compose.ContainerEvent
299314
}
300315

301-
func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { //nolint:unparam
302-
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
316+
func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, consumer compose.LogConsumer, stopFn func() error) (int, error) { //nolint:unparam
303317
var aborting bool
318+
var count int
304319
for {
305320
event := <-p.queue
306321
switch event.Type {
322+
case compose.ContainerEventAttach:
323+
consumer.Register(event.Service, event.Source)
324+
count++
307325
case compose.ContainerEventExit:
308326
if !aborting {
309327
consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
310328
}
311-
if cascadeStop && !aborting {
312-
aborting = true
313-
fmt.Println("Aborting on container exit...")
314-
err := stopFn()
315-
if err != nil {
316-
return 0, err
329+
if cascadeStop {
330+
if !aborting {
331+
aborting = true
332+
fmt.Println("Aborting on container exit...")
333+
err := stopFn()
334+
if err != nil {
335+
return 0, err
336+
}
337+
}
338+
if exitCodeFrom == "" || exitCodeFrom == event.Service {
339+
logrus.Error(event.ExitCode)
340+
return event.ExitCode, nil
317341
}
318342
}
319-
if exitCodeFrom == "" || exitCodeFrom == event.Service {
320-
logrus.Error(event.ExitCode)
321-
return event.ExitCode, nil
343+
count--
344+
if count == 0 {
345+
// Last container terminated, done
346+
return 0, nil
322347
}
323348
case compose.ContainerEventLog:
324349
if !aborting {

cli/formatter/colors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ var names = []string{
3535
// colorFunc use ANSI codes to render colored text on console
3636
type colorFunc func(s string) string
3737

38+
var monochrome = func(s string) string {
39+
return s
40+
}
41+
3842
func ansiColor(code, s string) string {
3943
return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
4044
}

cli/formatter/logs.go

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package formatter
1818

1919
import (
20-
"bytes"
2120
"context"
2221
"fmt"
2322
"io"
@@ -28,59 +27,91 @@ import (
2827
)
2928

3029
// NewLogConsumer creates a new LogConsumer
31-
func NewLogConsumer(ctx context.Context, w io.Writer) compose.LogConsumer {
30+
func NewLogConsumer(ctx context.Context, w io.Writer, color bool, prefix bool) compose.LogConsumer {
3231
return &logConsumer{
33-
ctx: ctx,
34-
colors: map[string]colorFunc{},
35-
width: 0,
36-
writer: w,
32+
ctx: ctx,
33+
presenters: map[string]*presenter{},
34+
width: 0,
35+
writer: w,
36+
color: color,
37+
prefix: prefix,
3738
}
3839
}
3940

41+
func (l *logConsumer) Register(service string, source string) {
42+
l.register(service, source)
43+
}
44+
45+
func (l *logConsumer) register(service string, source string) *presenter {
46+
cf := monochrome
47+
if l.color {
48+
cf = <-loop
49+
}
50+
p := &presenter{
51+
colors: cf,
52+
service: service,
53+
container: source,
54+
}
55+
l.presenters[source] = p
56+
if l.prefix {
57+
l.computeWidth()
58+
for _, p := range l.presenters {
59+
p.setPrefix(l.width)
60+
}
61+
}
62+
return p
63+
}
64+
4065
// Log formats a log message as received from service/container
4166
func (l *logConsumer) Log(service, container, message string) {
4267
if l.ctx.Err() != nil {
4368
return
4469
}
45-
cf := l.getColorFunc(service)
46-
prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container)
47-
70+
p, ok := l.presenters[container]
71+
if !ok { // should have been registered, but ¯\_(ツ)_/¯
72+
p = l.register(service, container)
73+
}
4874
for _, line := range strings.Split(message, "\n") {
49-
buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line))
50-
l.writer.Write(buf.Bytes()) // nolint:errcheck
75+
fmt.Fprintf(l.writer, "%s %s\n", p.prefix, line) // nolint:errcheck
5176
}
5277
}
5378

5479
func (l *logConsumer) Status(service, container, msg string) {
55-
cf := l.getColorFunc(service)
56-
buf := bytes.NewBufferString(cf(fmt.Sprintf("%s %s\n", container, msg)))
57-
l.writer.Write(buf.Bytes()) // nolint:errcheck
58-
}
59-
60-
func (l *logConsumer) getColorFunc(service string) colorFunc {
61-
cf, ok := l.colors[service]
80+
p, ok := l.presenters[container]
6281
if !ok {
63-
cf = <-loop
64-
l.colors[service] = cf
65-
l.computeWidth()
82+
p = l.register(service, container)
6683
}
67-
return cf
84+
s := p.colors(fmt.Sprintf("%s %s\n", container, msg))
85+
l.writer.Write([]byte(s)) // nolint:errcheck
6886
}
6987

7088
func (l *logConsumer) computeWidth() {
7189
width := 0
72-
for n := range l.colors {
90+
for n := range l.presenters {
7391
if len(n) > width {
7492
width = len(n)
7593
}
7694
}
77-
l.width = width + 3
95+
l.width = width + 1
7896
}
7997

8098
// LogConsumer consume logs from services and format them
8199
type logConsumer struct {
82-
ctx context.Context
83-
colors map[string]colorFunc
84-
width int
85-
writer io.Writer
100+
ctx context.Context
101+
presenters map[string]*presenter
102+
width int
103+
writer io.Writer
104+
color bool
105+
prefix bool
106+
}
107+
108+
type presenter struct {
109+
colors colorFunc
110+
service string
111+
container string
112+
prefix string
113+
}
114+
115+
func (p *presenter) setPrefix(width int) {
116+
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s |", p.container))
86117
}

local/compose/attach.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,21 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con
3636
return nil, err
3737
}
3838

39+
containers.sorted() // This enforce predictable colors assignment
40+
3941
var names []string
4042
for _, c := range containers {
4143
names = append(names, getCanonicalContainerName(c))
4244
}
45+
4346
fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
4447

4548
for _, container := range containers {
49+
consumer(compose.ContainerEvent{
50+
Type: compose.ContainerEventAttach,
51+
Source: getContainerNameWithoutProject(container),
52+
Service: container.Labels[serviceLabel],
53+
})
4654
err := s.attachContainer(ctx, container, consumer, project)
4755
if err != nil {
4856
return nil, err

local/compose/containers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package compose
1818

1919
import (
2020
"context"
21+
"sort"
2122

2223
"github.com/compose-spec/compose-go/types"
2324
moby "github.com/docker/docker/api/types"
@@ -83,3 +84,10 @@ func (containers Containers) forEach(fn func(moby.Container)) {
8384
fn(c)
8485
}
8586
}
87+
88+
func (containers Containers) sorted() Containers {
89+
sort.Slice(containers, func(i, j int) bool {
90+
return getCanonicalContainerName(containers[i]) < getCanonicalContainerName(containers[j])
91+
})
92+
return containers
93+
}

utils/logconsumer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ func (a *allowListLogConsumer) Status(service, container, message string) {
6464
}
6565
}
6666

67+
func (a *allowListLogConsumer) Register(service string, source string) {
68+
if a.allowList[service] {
69+
a.delegate.Register(service, source)
70+
}
71+
}
72+
6773
type splitBuffer struct {
6874
service string
6975
container string

0 commit comments

Comments
 (0)