Merge pull request #1285 from docker/predictable_colors

This commit is contained in:
Nicolas De loof 2021-02-12 20:31:48 +01:00 committed by GitHub
commit a69aa3d98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 74 deletions

View File

@ -184,6 +184,7 @@ type Stack struct {
type LogConsumer interface { type LogConsumer interface {
Log(service, container, message string) Log(service, container, message string)
Status(service, container, msg string) Status(service, container, msg string)
Register(service string, source string)
} }
// ContainerEventListener is a callback to process ContainerEvent from services // ContainerEventListener is a callback to process ContainerEvent from services
@ -201,6 +202,8 @@ type ContainerEvent struct {
const ( const (
// ContainerEventLog is a ContainerEvent of type log. Line is set // ContainerEventLog is a ContainerEvent of type log. Line is set
ContainerEventLog = iota 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 is a ContainerEvent of type exit. ExitCode is set
ContainerEventExit ContainerEventExit
) )

View File

@ -31,8 +31,10 @@ import (
type logsOptions struct { type logsOptions struct {
*projectOptions *projectOptions
composeOptions composeOptions
follow bool follow bool
tail string tail string
noColor bool
noPrefix bool
} }
func logsCommand(p *projectOptions, contextType string) *cobra.Command { func logsCommand(p *projectOptions, contextType string) *cobra.Command {
@ -46,9 +48,13 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command {
return runLogs(cmd.Context(), opts, args) 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 { 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 return logsCmd
} }
@ -63,7 +69,7 @@ func runLogs(ctx context.Context, opts logsOptions, services []string) error {
if err != nil { if err != nil {
return err 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{ return c.ComposeService().Logs(ctx, projectName, consumer, compose.LogOptions{
Services: services, Services: services,
Follow: opts.follow, Follow: opts.follow,

View File

@ -28,7 +28,6 @@ import (
type startOptions struct { type startOptions struct {
*projectOptions *projectOptions
Detach bool
} }
func startCommand(p *projectOptions) *cobra.Command { func startCommand(p *projectOptions) *cobra.Command {
@ -42,8 +41,6 @@ func startCommand(p *projectOptions) *cobra.Command {
return runStart(cmd.Context(), opts, args) return runStart(cmd.Context(), opts, args)
}, },
} }
startCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
return startCmd return startCmd
} }
@ -58,32 +55,8 @@ func runStart(ctx context.Context, opts startOptions, services []string) error {
return err return err
} }
if opts.Detach { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) { return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
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
}) })
return err return err
} }

View File

@ -36,6 +36,7 @@ import (
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
) )
// composeOptions hold options common to `up` and `run` to run compose project // composeOptions hold options common to `up` and `run` to run compose project
@ -57,6 +58,8 @@ type upOptions struct {
cascadeStop bool cascadeStop bool
exitCodeFrom string exitCodeFrom string
scale []string scale []string
noColor bool
noPrefix bool
} }
func (o upOptions) recreateStrategy() string { func (o upOptions) recreateStrategy() string {
@ -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.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.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.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 { switch contextType {
case store.AciContextType: case store.AciContextType:
@ -199,6 +204,16 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
stopFunc() // nolint:errcheck 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{ err = c.ComposeService().Start(ctx, project, compose.StartOptions{
Attach: func(event compose.ContainerEvent) { Attach: func(event compose.ContainerEvent) {
queue <- event queue <- event
@ -208,7 +223,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
return err return err
} }
exitCode, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, stopFunc) err = eg.Wait()
if exitCode != 0 { if exitCode != 0 {
return cmd.ExitCodeError{ExitCode: exitCode} return cmd.ExitCodeError{ExitCode: exitCode}
} }
@ -298,27 +313,37 @@ type printer struct {
queue chan compose.ContainerEvent queue chan compose.ContainerEvent
} }
func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { //nolint:unparam func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, consumer compose.LogConsumer, stopFn func() error) (int, error) { //nolint:unparam
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
var aborting bool var aborting bool
var count int
for { for {
event := <-p.queue event := <-p.queue
switch event.Type { switch event.Type {
case compose.ContainerEventAttach:
consumer.Register(event.Service, event.Source)
count++
case compose.ContainerEventExit: case compose.ContainerEventExit:
if !aborting { if !aborting {
consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode)) consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
} }
if cascadeStop && !aborting { if cascadeStop {
aborting = true if !aborting {
fmt.Println("Aborting on container exit...") aborting = true
err := stopFn() fmt.Println("Aborting on container exit...")
if err != nil { err := stopFn()
return 0, err if err != nil {
return 0, err
}
}
if exitCodeFrom == "" || exitCodeFrom == event.Service {
logrus.Error(event.ExitCode)
return event.ExitCode, nil
} }
} }
if exitCodeFrom == "" || exitCodeFrom == event.Service { count--
logrus.Error(event.ExitCode) if count == 0 {
return event.ExitCode, nil // Last container terminated, done
return 0, nil
} }
case compose.ContainerEventLog: case compose.ContainerEventLog:
if !aborting { if !aborting {

View File

@ -35,6 +35,10 @@ var names = []string{
// colorFunc use ANSI codes to render colored text on console // colorFunc use ANSI codes to render colored text on console
type colorFunc func(s string) string type colorFunc func(s string) string
var monochrome = func(s string) string {
return s
}
func ansiColor(code, s string) string { func ansiColor(code, s string) string {
return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0")) return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
} }

View File

@ -17,7 +17,6 @@
package formatter package formatter
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -28,59 +27,91 @@ import (
) )
// NewLogConsumer creates a new LogConsumer // 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{ return &logConsumer{
ctx: ctx, ctx: ctx,
colors: map[string]colorFunc{}, presenters: map[string]*presenter{},
width: 0, width: 0,
writer: w, 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 // Log formats a log message as received from service/container
func (l *logConsumer) Log(service, container, message string) { func (l *logConsumer) Log(service, container, message string) {
if l.ctx.Err() != nil { if l.ctx.Err() != nil {
return return
} }
cf := l.getColorFunc(service) p, ok := l.presenters[container]
prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container) if !ok { // should have been registered, but ¯\_(ツ)_/¯
p = l.register(service, container)
}
for _, line := range strings.Split(message, "\n") { for _, line := range strings.Split(message, "\n") {
buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line)) fmt.Fprintf(l.writer, "%s %s\n", p.prefix, line) // nolint:errcheck
l.writer.Write(buf.Bytes()) // nolint:errcheck
} }
} }
func (l *logConsumer) Status(service, container, msg string) { func (l *logConsumer) Status(service, container, msg string) {
cf := l.getColorFunc(service) p, ok := l.presenters[container]
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]
if !ok { if !ok {
cf = <-loop p = l.register(service, container)
l.colors[service] = cf
l.computeWidth()
} }
return cf s := p.colors(fmt.Sprintf("%s %s\n", container, msg))
l.writer.Write([]byte(s)) // nolint:errcheck
} }
func (l *logConsumer) computeWidth() { func (l *logConsumer) computeWidth() {
width := 0 width := 0
for n := range l.colors { for n := range l.presenters {
if len(n) > width { if len(n) > width {
width = len(n) width = len(n)
} }
} }
l.width = width + 3 l.width = width + 1
} }
// LogConsumer consume logs from services and format them // LogConsumer consume logs from services and format them
type logConsumer struct { type logConsumer struct {
ctx context.Context ctx context.Context
colors map[string]colorFunc presenters map[string]*presenter
width int width int
writer io.Writer 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))
} }

View File

@ -36,13 +36,21 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con
return nil, err return nil, err
} }
containers.sorted() // This enforce predictable colors assignment
var names []string var names []string
for _, c := range containers { for _, c := range containers {
names = append(names, getCanonicalContainerName(c)) names = append(names, getCanonicalContainerName(c))
} }
fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
for _, container := range containers { for _, container := range containers {
consumer(compose.ContainerEvent{
Type: compose.ContainerEventAttach,
Source: getContainerNameWithoutProject(container),
Service: container.Labels[serviceLabel],
})
err := s.attachContainer(ctx, container, consumer, project) err := s.attachContainer(ctx, container, consumer, project)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -18,6 +18,7 @@ package compose
import ( import (
"context" "context"
"sort"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
@ -83,3 +84,10 @@ func (containers Containers) forEach(fn func(moby.Container)) {
fn(c) fn(c)
} }
} }
func (containers Containers) sorted() Containers {
sort.Slice(containers, func(i, j int) bool {
return getCanonicalContainerName(containers[i]) < getCanonicalContainerName(containers[j])
})
return containers
}

View File

@ -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 { type splitBuffer struct {
service string service string
container string container string