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 }