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 }