mirror of
https://github.com/docker/compose.git
synced 2025-07-26 07:04:32 +02:00
Merge pull request #1285 from docker/predictable_colors
This commit is contained in:
commit
a69aa3d98a
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user