github.com/containerd/nerdctl@v1.7.7/pkg/composer/logs.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package composer
    18  
    19  import (
    20  	"context"
    21  	"os"
    22  	"os/exec"
    23  	"os/signal"
    24  	"strings"
    25  
    26  	"github.com/compose-spec/compose-go/types"
    27  	"github.com/containerd/containerd"
    28  	"github.com/containerd/log"
    29  	"github.com/containerd/nerdctl/pkg/composer/pipetagger"
    30  	"github.com/containerd/nerdctl/pkg/composer/serviceparser"
    31  	"github.com/containerd/nerdctl/pkg/labels"
    32  )
    33  
    34  type LogsOptions struct {
    35  	Follow      bool
    36  	Timestamps  bool
    37  	Tail        string
    38  	NoColor     bool
    39  	NoLogPrefix bool
    40  }
    41  
    42  func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string) error {
    43  	var serviceNames []string
    44  	err := c.project.WithServices(services, func(svc types.ServiceConfig) error {
    45  		serviceNames = append(serviceNames, svc.Name)
    46  		return nil
    47  	}, types.IgnoreDependencies)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	containers, err := c.Containers(ctx, serviceNames...)
    52  	if err != nil {
    53  		return err
    54  	}
    55  	return c.logs(ctx, containers, lo)
    56  }
    57  
    58  func (c *Composer) logs(ctx context.Context, containers []containerd.Container, lo LogsOptions) error {
    59  	var logTagMaxLen int
    60  	type containerState struct {
    61  		name   string
    62  		logTag string
    63  		logCmd *exec.Cmd
    64  	}
    65  
    66  	containerStates := make(map[string]containerState, len(containers)) // key: containerID
    67  	for _, container := range containers {
    68  		info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
    69  		if err != nil {
    70  			return err
    71  		}
    72  		name := info.Labels[labels.Name]
    73  		logTag := strings.TrimPrefix(name, c.project.Name+serviceparser.Separator)
    74  		if l := len(logTag); l > logTagMaxLen {
    75  			logTagMaxLen = l
    76  		}
    77  		containerStates[container.ID()] = containerState{
    78  			name:   name,
    79  			logTag: logTag,
    80  		}
    81  	}
    82  
    83  	logsEOFChan := make(chan string) // value: container name
    84  	for id, state := range containerStates {
    85  		// TODO: show logs without executing `nerdctl logs`
    86  		args := []string{"logs"}
    87  		if lo.Follow {
    88  			args = append(args, "-f")
    89  		}
    90  		if lo.Timestamps {
    91  			args = append(args, "-t")
    92  		}
    93  		if lo.Tail != "" {
    94  			args = append(args, "-n")
    95  			if lo.Tail == "all" {
    96  				args = append(args, "+0")
    97  			} else {
    98  				args = append(args, lo.Tail)
    99  			}
   100  		}
   101  
   102  		args = append(args, id)
   103  		state.logCmd = c.createNerdctlCmd(ctx, args...)
   104  		stdout, err := state.logCmd.StdoutPipe()
   105  		if err != nil {
   106  			return err
   107  		}
   108  		logWidth := logTagMaxLen + 1
   109  		if lo.NoLogPrefix {
   110  			logWidth = -1
   111  		}
   112  		stdoutTagger := pipetagger.New(os.Stdout, stdout, state.logTag, logWidth, lo.NoColor)
   113  		stderr, err := state.logCmd.StderrPipe()
   114  		if err != nil {
   115  			return err
   116  		}
   117  		stderrTagger := pipetagger.New(os.Stderr, stderr, state.logTag, logWidth, lo.NoColor)
   118  		if c.DebugPrintFull {
   119  			log.G(ctx).Debugf("Running %v", state.logCmd.Args)
   120  		}
   121  		if err := state.logCmd.Start(); err != nil {
   122  			return err
   123  		}
   124  		containerName := state.name
   125  		go func() {
   126  			stdoutTagger.Run()
   127  			logsEOFChan <- containerName
   128  		}()
   129  		go stderrTagger.Run()
   130  	}
   131  
   132  	interruptChan := make(chan os.Signal, 1)
   133  	signal.Notify(interruptChan, os.Interrupt)
   134  
   135  	logsEOFMap := make(map[string]struct{}) // key: container name
   136  selectLoop:
   137  	for {
   138  		// Wait for Ctrl-C, or `nerdctl compose down` in another terminal
   139  		select {
   140  		case sig := <-interruptChan:
   141  			log.G(ctx).Debugf("Received signal: %s", sig)
   142  			break selectLoop
   143  		case containerName := <-logsEOFChan:
   144  			if lo.Follow {
   145  				// When `nerdctl logs -f` has exited, we can assume that the container has exited
   146  				log.G(ctx).Infof("Container %q exited", containerName)
   147  			} else {
   148  				log.G(ctx).Debugf("Logs for container %q reached EOF", containerName)
   149  			}
   150  			logsEOFMap[containerName] = struct{}{}
   151  			if len(logsEOFMap) == len(containerStates) {
   152  				if lo.Follow {
   153  					log.G(ctx).Info("All the containers have exited")
   154  				} else {
   155  					log.G(ctx).Debug("All the logs reached EOF")
   156  				}
   157  				break selectLoop
   158  			}
   159  		}
   160  	}
   161  
   162  	for _, state := range containerStates {
   163  		if state.logCmd != nil && state.logCmd.Process != nil {
   164  			if err := state.logCmd.Process.Kill(); err != nil {
   165  				log.G(ctx).Warn(err)
   166  			}
   167  		}
   168  	}
   169  
   170  	return nil
   171  }