github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/logger/log.go (about) 1 /* 2 Copyright 2019 The Skaffold 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 logger 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "sync" 24 "sync/atomic" 25 "time" 26 27 v1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/watch" 29 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log/stream" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 36 olog "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 37 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 38 ) 39 40 type Logger interface { 41 log.Logger 42 GetFormatter() Formatter 43 } 44 45 // LogAggregator aggregates the logs for all the deployed pods. 46 type LogAggregator struct { 47 output io.Writer 48 kubectlcli *kubectl.CLI 49 config Config 50 podSelector kubernetes.PodSelector 51 podWatcher kubernetes.PodWatcher 52 colorPicker output.ColorPicker 53 formatter Formatter 54 muted int32 55 stopWatcher func() 56 sinceTime time.Time 57 events chan kubernetes.PodEvent 58 trackedContainers trackedContainers 59 namespaces *[]string 60 } 61 62 type Config interface { 63 Tail() bool 64 PipelineForImage(imageName string) (latest.Pipeline, bool) 65 DefaultPipeline() latest.Pipeline 66 JSONParseConfig() latest.JSONParseConfig 67 } 68 69 // NewLogAggregator creates a new LogAggregator for a given output. 70 func NewLogAggregator(cli *kubectl.CLI, podSelector kubernetes.PodSelector, namespaces *[]string, config Config) *LogAggregator { 71 a := &LogAggregator{ 72 kubectlcli: cli, 73 config: config, 74 podSelector: podSelector, 75 podWatcher: kubernetes.NewPodWatcher(podSelector), 76 colorPicker: output.NewColorPicker(), 77 stopWatcher: func() {}, 78 events: make(chan kubernetes.PodEvent), 79 namespaces: namespaces, 80 } 81 a.formatter = func(p v1.Pod, c v1.ContainerStatus, isMuted func() bool) log.Formatter { 82 pod := p 83 return newKubernetesLogFormatter(config, a.colorPicker, isMuted, &pod, c) 84 } 85 return a 86 } 87 88 func (a *LogAggregator) GetFormatter() Formatter { 89 return a.formatter 90 } 91 92 // RegisterArtifacts tracks the provided build artifacts in the colorpicker 93 func (a *LogAggregator) RegisterArtifacts(artifacts []graph.Artifact) { 94 // image tags are added to the podSelector by the deployer, which are picked up by the podWatcher 95 // we just need to make sure the colorPicker knows about the base images. 96 // artifact.ImageName does not have a default repo substitution applied to it, so we use artifact.Tag. 97 // TODO(nkubala) [07/15/22]: can we apply default repo to artifact.Image and avoid stripping tags? 98 for _, artifact := range artifacts { 99 a.colorPicker.AddImage(artifact.Tag) 100 } 101 } 102 103 func (a *LogAggregator) SetSince(t time.Time) { 104 if a == nil { 105 // Logs are not activated. 106 return 107 } 108 109 a.sinceTime = t 110 } 111 112 // Start starts a logger that listens to pods and tail their logs 113 // if they are matched by the `podSelector`. 114 func (a *LogAggregator) Start(ctx context.Context, out io.Writer) error { 115 if a == nil { 116 // Logs are not activated. 117 return nil 118 } 119 120 a.output = out 121 122 a.podWatcher.Register(a.events) 123 stopWatcher, err := a.podWatcher.Start(ctx, a.kubectlcli.KubeContext, *a.namespaces) 124 if err != nil { 125 return err 126 } 127 128 go func() { 129 defer stopWatcher() 130 l := olog.Entry(ctx) 131 defer l.Tracef("logAggregator: cease waiting for pod events") 132 l.Tracef("logAggregator: waiting for pod events") 133 for { 134 select { 135 case <-ctx.Done(): 136 l.Tracef("logAggregator: context canceled, ignoring") 137 case evt, ok := <-a.events: 138 if !ok { 139 l.Tracef("logAggregator: channel closed, returning") 140 return 141 } 142 143 // TODO(dgageot): Add EphemeralContainerStatuses 144 pod := evt.Pod 145 if evt.Type == watch.Deleted { 146 continue 147 } 148 149 for _, c := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { 150 if c.ContainerID == "" { 151 if c.State.Waiting != nil && c.State.Waiting.Message != "" { 152 output.Red.Fprintln(a.output, c.State.Waiting.Message) 153 } 154 continue 155 } 156 157 if !a.trackedContainers.add(c.ContainerID) && a.config.Tail() { 158 go a.streamContainerLogs(ctx, pod, c) 159 } 160 } 161 } 162 } 163 }() 164 165 return nil 166 } 167 168 // Stop stops the logger. 169 func (a *LogAggregator) Stop() { 170 l := olog.Entry(context.Background()) 171 if a == nil { 172 // Logs are not activated. 173 return 174 } 175 a.podWatcher.Deregister(a.events) 176 l.Tracef("logAggregator: Stop() close channel") 177 close(a.events) // the receiver shouldn't really be the one to close the channel 178 } 179 180 func sinceSeconds(d time.Duration) int64 { 181 since := int64((d + 999*time.Millisecond).Truncate(1 * time.Second).Seconds()) 182 if since != 0 { 183 return since 184 } 185 186 // 0 means all the logs. So we ask for the logs since 1s. 187 return 1 188 } 189 190 func (a *LogAggregator) streamContainerLogs(ctx context.Context, pod *v1.Pod, container v1.ContainerStatus) { 191 olog.Entry(ctx).Infof("Streaming logs from pod: %s container: %s", pod.Name, container.Name) 192 193 // In theory, it's more precise to use --since-time='' but there can be a time 194 // difference between the user's machine and the server. 195 // So we use --since=Xs and round up to the nearest second to not lose any log. 196 sinceSeconds := fmt.Sprintf("--since=%ds", sinceSeconds(time.Since(a.sinceTime))) 197 198 tr, tw := io.Pipe() 199 go func() { 200 if err := a.kubectlcli.Run(ctx, nil, tw, "logs", sinceSeconds, "-f", pod.Name, "-c", container.Name, "--namespace", pod.Namespace); err != nil { 201 // Don't print errors if the user interrupted the logs 202 // or if the logs were interrupted because of a configuration change 203 if ctx.Err() != context.Canceled { 204 olog.Entry(ctx).Warn(err) 205 } 206 } 207 _ = tw.Close() 208 }() 209 210 if err := stream.StreamRequest(ctx, a.output, a.formatter(*pod, container, a.IsMuted), tr); err != nil { 211 olog.Entry(ctx).Errorf("streaming request %s", err) 212 } 213 } 214 215 // Mute mutes the logs. 216 func (a *LogAggregator) Mute() { 217 if a == nil { 218 // Logs are not activated. 219 return 220 } 221 222 atomic.StoreInt32(&a.muted, 1) 223 } 224 225 // Unmute unmutes the logs. 226 func (a *LogAggregator) Unmute() { 227 if a == nil { 228 // Logs are not activated. 229 return 230 } 231 atomic.StoreInt32(&a.muted, 0) 232 } 233 234 // IsMuted says if the logs are to be muted. 235 func (a *LogAggregator) IsMuted() bool { 236 return atomic.LoadInt32(&a.muted) == 1 237 } 238 239 type trackedContainers struct { 240 sync.Mutex 241 ids map[string]bool 242 } 243 244 // add adds a containerID to be tracked. Return true if the container 245 // was already tracked. 246 func (t *trackedContainers) add(id string) bool { 247 t.Lock() 248 defer t.Unlock() 249 alreadyTracked := t.ids[id] 250 if t.ids == nil { 251 t.ids = map[string]bool{} 252 } 253 t.ids[id] = true 254 255 return alreadyTracked 256 } 257 258 // NoopLogger is used in tests. It will never retrieve any logs from any resources. 259 type NoopLogger struct { 260 *log.NoopLogger 261 } 262 263 func (*NoopLogger) GetFormatter() Formatter { return nil }