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

Commit

Permalink
Merge pull request #1285 from docker/predictable_colors
Browse files Browse the repository at this point in the history
  • Loading branch information
ndeloof authored Feb 12, 2021
2 parents 54a7819 + 4e5734f commit a69aa3d
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 74 deletions.
3 changes: 3 additions & 0 deletions api/compose/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ type Stack struct {
type LogConsumer interface {
Log(service, container, message string)
Status(service, container, msg string)
Register(service string, source string)
}

// ContainerEventListener is a callback to process ContainerEvent from services
Expand All @@ -201,6 +202,8 @@ type ContainerEvent struct {
const (
// ContainerEventLog is a ContainerEvent of type log. Line is set
ContainerEventLog = iota
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
ContainerEventAttach
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
ContainerEventExit
)
16 changes: 11 additions & 5 deletions cli/cmd/compose/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import (
type logsOptions struct {
*projectOptions
composeOptions
follow bool
tail string
follow bool
tail string
noColor bool
noPrefix bool
}

func logsCommand(p *projectOptions, contextType string) *cobra.Command {
Expand All @@ -46,9 +48,13 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command {
return runLogs(cmd.Context(), opts, args)
},
}
logsCmd.Flags().BoolVar(&opts.follow, "follow", false, "Follow log output.")
flags := logsCmd.Flags()
flags.BoolVar(&opts.follow, "follow", false, "Follow log output.")
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.")
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")

if contextType == store.DefaultContextType {
logsCmd.Flags().StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs for each container.")
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs for each container.")
}
return logsCmd
}
Expand All @@ -63,7 +69,7 @@ func runLogs(ctx context.Context, opts logsOptions, services []string) error {
if err != nil {
return err
}
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
return c.ComposeService().Logs(ctx, projectName, consumer, compose.LogOptions{
Services: services,
Follow: opts.follow,
Expand Down
31 changes: 2 additions & 29 deletions cli/cmd/compose/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

type startOptions struct {
*projectOptions
Detach bool
}

func startCommand(p *projectOptions) *cobra.Command {
Expand All @@ -42,8 +41,6 @@ func startCommand(p *projectOptions) *cobra.Command {
return runStart(cmd.Context(), opts, args)
},
}

startCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
return startCmd
}

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

if opts.Detach {
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
})
return err
}

queue := make(chan compose.ContainerEvent)
printer := printer{
queue: queue,
}
err = c.ComposeService().Start(ctx, project, compose.StartOptions{
Attach: func(event compose.ContainerEvent) {
queue <- event
},
})
if err != nil {
return err
}

_, err = printer.run(ctx, false, "", func() error {
ctx := context.Background()
_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
return "", c.ComposeService().Stop(ctx, project)
})
return err
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
})
return err
}
49 changes: 37 additions & 12 deletions cli/cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)

// composeOptions hold options common to `up` and `run` to run compose project
Expand All @@ -57,6 +58,8 @@ type upOptions struct {
cascadeStop bool
exitCodeFrom string
scale []string
noColor bool
noPrefix bool
}

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

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

consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)

var exitCode int
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
code, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, consumer, stopFunc)
exitCode = code
return err
})

err = c.ComposeService().Start(ctx, project, compose.StartOptions{
Attach: func(event compose.ContainerEvent) {
queue <- event
Expand All @@ -208,7 +223,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
return err
}

exitCode, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, stopFunc)
err = eg.Wait()
if exitCode != 0 {
return cmd.ExitCodeError{ExitCode: exitCode}
}
Expand Down Expand Up @@ -298,27 +313,37 @@ type printer struct {
queue chan compose.ContainerEvent
}

func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { //nolint:unparam
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, consumer compose.LogConsumer, stopFn func() error) (int, error) { //nolint:unparam
var aborting bool
var count int
for {
event := <-p.queue
switch event.Type {
case compose.ContainerEventAttach:
consumer.Register(event.Service, event.Source)
count++
case compose.ContainerEventExit:
if !aborting {
consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
}
if cascadeStop && !aborting {
aborting = true
fmt.Println("Aborting on container exit...")
err := stopFn()
if err != nil {
return 0, err
if cascadeStop {
if !aborting {
aborting = true
fmt.Println("Aborting on container exit...")
err := stopFn()
if err != nil {
return 0, err
}
}
if exitCodeFrom == "" || exitCodeFrom == event.Service {
logrus.Error(event.ExitCode)
return event.ExitCode, nil
}
}
if exitCodeFrom == "" || exitCodeFrom == event.Service {
logrus.Error(event.ExitCode)
return event.ExitCode, nil
count--
if count == 0 {
// Last container terminated, done
return 0, nil
}
case compose.ContainerEventLog:
if !aborting {
Expand Down
4 changes: 4 additions & 0 deletions cli/formatter/colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ var names = []string{
// colorFunc use ANSI codes to render colored text on console
type colorFunc func(s string) string

var monochrome = func(s string) string {
return s
}

func ansiColor(code, s string) string {
return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
}
Expand Down
87 changes: 59 additions & 28 deletions cli/formatter/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package formatter

import (
"bytes"
"context"
"fmt"
"io"
Expand All @@ -28,59 +27,91 @@ import (
)

// NewLogConsumer creates a new LogConsumer
func NewLogConsumer(ctx context.Context, w io.Writer) compose.LogConsumer {
func NewLogConsumer(ctx context.Context, w io.Writer, color bool, prefix bool) compose.LogConsumer {
return &logConsumer{
ctx: ctx,
colors: map[string]colorFunc{},
width: 0,
writer: w,
ctx: ctx,
presenters: map[string]*presenter{},
width: 0,
writer: w,
color: color,
prefix: prefix,
}
}

func (l *logConsumer) Register(service string, source string) {
l.register(service, source)
}

func (l *logConsumer) register(service string, source string) *presenter {
cf := monochrome
if l.color {
cf = <-loop
}
p := &presenter{
colors: cf,
service: service,
container: source,
}
l.presenters[source] = p
if l.prefix {
l.computeWidth()
for _, p := range l.presenters {
p.setPrefix(l.width)
}
}
return p
}

// Log formats a log message as received from service/container
func (l *logConsumer) Log(service, container, message string) {
if l.ctx.Err() != nil {
return
}
cf := l.getColorFunc(service)
prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container)

p, ok := l.presenters[container]
if !ok { // should have been registered, but ¯\_(ツ)_/¯
p = l.register(service, container)
}
for _, line := range strings.Split(message, "\n") {
buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line))
l.writer.Write(buf.Bytes()) // nolint:errcheck
fmt.Fprintf(l.writer, "%s %s\n", p.prefix, line) // nolint:errcheck
}
}

func (l *logConsumer) Status(service, container, msg string) {
cf := l.getColorFunc(service)
buf := bytes.NewBufferString(cf(fmt.Sprintf("%s %s\n", container, msg)))
l.writer.Write(buf.Bytes()) // nolint:errcheck
}

func (l *logConsumer) getColorFunc(service string) colorFunc {
cf, ok := l.colors[service]
p, ok := l.presenters[container]
if !ok {
cf = <-loop
l.colors[service] = cf
l.computeWidth()
p = l.register(service, container)
}
return cf
s := p.colors(fmt.Sprintf("%s %s\n", container, msg))
l.writer.Write([]byte(s)) // nolint:errcheck
}

func (l *logConsumer) computeWidth() {
width := 0
for n := range l.colors {
for n := range l.presenters {
if len(n) > width {
width = len(n)
}
}
l.width = width + 3
l.width = width + 1
}

// LogConsumer consume logs from services and format them
type logConsumer struct {
ctx context.Context
colors map[string]colorFunc
width int
writer io.Writer
ctx context.Context
presenters map[string]*presenter
width int
writer io.Writer
color bool
prefix bool
}

type presenter struct {
colors colorFunc
service string
container string
prefix string
}

func (p *presenter) setPrefix(width int) {
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s |", p.container))
}
8 changes: 8 additions & 0 deletions local/compose/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,21 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con
return nil, err
}

containers.sorted() // This enforce predictable colors assignment

var names []string
for _, c := range containers {
names = append(names, getCanonicalContainerName(c))
}

fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))

for _, container := range containers {
consumer(compose.ContainerEvent{
Type: compose.ContainerEventAttach,
Source: getContainerNameWithoutProject(container),
Service: container.Labels[serviceLabel],
})
err := s.attachContainer(ctx, container, consumer, project)
if err != nil {
return nil, err
Expand Down
8 changes: 8 additions & 0 deletions local/compose/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package compose

import (
"context"
"sort"

"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types"
Expand Down Expand Up @@ -83,3 +84,10 @@ func (containers Containers) forEach(fn func(moby.Container)) {
fn(c)
}
}

func (containers Containers) sorted() Containers {
sort.Slice(containers, func(i, j int) bool {
return getCanonicalContainerName(containers[i]) < getCanonicalContainerName(containers[j])
})
return containers
}
6 changes: 6 additions & 0 deletions utils/logconsumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func (a *allowListLogConsumer) Status(service, container, message string) {
}
}

func (a *allowListLogConsumer) Register(service string, source string) {
if a.allowList[service] {
a.delegate.Register(service, source)
}
}

type splitBuffer struct {
service string
container string
Expand Down

0 comments on commit a69aa3d

Please sign in to comment.