knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/logstream/v2/stream.go (about)

     1  /*
     2  Copyright 2020 The Knative 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 logstream
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"reflect"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	"k8s.io/client-go/kubernetes"
    35  	"knative.dev/pkg/ptr"
    36  )
    37  
    38  // New creates a new log source. The source namespaces must be configured through
    39  // log source options.
    40  func New(ctx context.Context, c kubernetes.Interface, opts ...func(*logSource)) Source {
    41  	s := &logSource{
    42  		ctx:         ctx,
    43  		kc:          c,
    44  		keys:        make(map[string]Callback, 1),
    45  		filterLines: true, // Filtering log lines by the watched resource name is enabled by default.
    46  	}
    47  	for _, opt := range opts {
    48  		opt(s)
    49  	}
    50  	return s
    51  }
    52  
    53  // WithNamespaces configures namespaces for log stream.
    54  func WithNamespaces(namespaces ...string) func(*logSource) {
    55  	return func(s *logSource) {
    56  		s.namespaces = namespaces
    57  	}
    58  }
    59  
    60  // WithLineFiltering configures whether log lines will be filtered by
    61  // the resource name.
    62  func WithLineFiltering(enabled bool) func(*logSource) {
    63  	return func(s *logSource) {
    64  		s.filterLines = enabled
    65  	}
    66  }
    67  
    68  // WithPodPrefixes specifies which Pods will be included in the
    69  // log stream through the provided prefixes. If no prefixes are
    70  // configured then logs from all Pods in the configured namespaces will
    71  // be streamed.
    72  func WithPodPrefixes(podPrefixes ...string) func(*logSource) {
    73  	return func(s *logSource) {
    74  		s.podPrefixes = podPrefixes
    75  	}
    76  }
    77  
    78  func FromNamespaces(ctx context.Context, c kubernetes.Interface, namespaces []string, opts ...func(*logSource)) Source {
    79  	sOpts := []func(*logSource){WithNamespaces(namespaces...)}
    80  	sOpts = append(sOpts, opts...)
    81  	return New(ctx, c, sOpts...)
    82  }
    83  
    84  func FromNamespace(ctx context.Context, c kubernetes.Interface, namespace string, opts ...func(*logSource)) Source {
    85  	return FromNamespaces(ctx, c, []string{namespace}, opts...)
    86  }
    87  
    88  type logSource struct {
    89  	namespaces []string
    90  	kc         kubernetes.Interface
    91  	ctx        context.Context
    92  
    93  	m           sync.RWMutex
    94  	once        sync.Once
    95  	keys        map[string]Callback
    96  	filterLines bool
    97  	podPrefixes []string
    98  	watchErr    error
    99  }
   100  
   101  func (s *logSource) StartStream(name string, l Callback) (Canceler, error) {
   102  	s.once.Do(func() { s.watchErr = s.watchPods() })
   103  	if s.watchErr != nil {
   104  		return nil, fmt.Errorf("failed to watch pods in one of the namespace(s) %q: %w", s.namespaces, s.watchErr)
   105  	}
   106  
   107  	// Register a key
   108  	s.m.Lock()
   109  	defer s.m.Unlock()
   110  	s.keys[name] = l
   111  
   112  	// Return a function that unregisters that key.
   113  	return func() {
   114  		s.m.Lock()
   115  		defer s.m.Unlock()
   116  		delete(s.keys, name)
   117  	}, nil
   118  }
   119  
   120  func (s *logSource) watchPods() error {
   121  	if len(s.namespaces) == 0 {
   122  		return errors.New("namespaces for logstream not configured")
   123  	}
   124  	for _, ns := range s.namespaces {
   125  		wi, err := s.kc.CoreV1().Pods(ns).Watch(s.ctx, metav1.ListOptions{})
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  		go func() {
   131  			defer wi.Stop()
   132  			watchedPods := sets.NewString()
   133  
   134  			for {
   135  				select {
   136  				case <-s.ctx.Done():
   137  					return
   138  				case ev := <-wi.ResultChan():
   139  					// We have reports of this being randomly nil.
   140  					if ev.Object == nil || reflect.ValueOf(ev.Object).IsNil() {
   141  						continue
   142  					}
   143  					p, ok := ev.Object.(*corev1.Pod)
   144  					if !ok {
   145  						// The Watch interface can return errors via the channel as *metav1.Status.
   146  						// Log those to get notified that loglines might be missing but don't crash.
   147  						s.handleGenericLine([]byte(fmt.Sprintf("unexpected event: %v", p)), "no-pod", "no-container")
   148  						continue
   149  					}
   150  					switch ev.Type {
   151  					case watch.Deleted:
   152  						watchedPods.Delete(p.Name)
   153  					case watch.Added, watch.Modified:
   154  						if !watchedPods.Has(p.Name) && isPodReady(p) && s.matchesPodPrefix(p.Name) {
   155  							watchedPods.Insert(p.Name)
   156  							s.startForPod(p)
   157  						}
   158  					}
   159  				}
   160  			}
   161  		}()
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  func (s *logSource) matchesPodPrefix(name string) bool {
   168  	if len(s.podPrefixes) == 0 {
   169  		// Pod prefixes are not configured => always match.
   170  		return true
   171  	}
   172  	for _, p := range s.podPrefixes {
   173  		if strings.Contains(name, p) {
   174  			return true
   175  		}
   176  	}
   177  	return false
   178  }
   179  
   180  func (s *logSource) startForPod(pod *corev1.Pod) {
   181  	// Grab data from all containers in the pods.  We need this in case
   182  	// an envoy sidecar is injected for mesh installs.  This should be
   183  	// equivalent to --all-containers.
   184  	for _, container := range pod.Spec.Containers {
   185  		// Required for capture below.
   186  		psn, pn, cn := pod.Namespace, pod.Name, container.Name
   187  
   188  		handleLine := s.handleLine
   189  		if wellKnownContainers.Has(cn) || !s.filterLines {
   190  			// Specialcase logs from chaosduck, queueproxy etc.
   191  			// - ChaosDuck logs enable easy
   192  			//   monitoring of killed pods throughout all tests.
   193  			// - QueueProxy logs enable
   194  			//   debugging troubleshooting data plane request handling issues.
   195  			handleLine = s.handleGenericLine
   196  		}
   197  
   198  		go func() {
   199  			options := &corev1.PodLogOptions{
   200  				Container: cn,
   201  				// Follow directs the API server to continuously stream logs back.
   202  				Follow: true,
   203  				// Only return new logs (this value is being used for "epsilon").
   204  				SinceSeconds: ptr.Int64(1),
   205  			}
   206  
   207  			req := s.kc.CoreV1().Pods(psn).GetLogs(pn, options)
   208  			stream, err := req.Stream(context.Background())
   209  			if err != nil {
   210  				s.handleGenericLine([]byte(err.Error()), pn, cn)
   211  				return
   212  			}
   213  			defer stream.Close()
   214  			// Read this container's stream.
   215  			for scanner := bufio.NewScanner(stream); scanner.Scan(); {
   216  				handleLine(scanner.Bytes(), pn, cn)
   217  			}
   218  			// Pods get killed with chaos duck, so logs might end
   219  			// before the test does. So don't report an error here.
   220  		}()
   221  	}
   222  }
   223  
   224  func isPodReady(p *corev1.Pod) bool {
   225  	if p.Status.Phase == corev1.PodRunning && p.DeletionTimestamp == nil {
   226  		for _, cond := range p.Status.Conditions {
   227  			if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
   228  				return true
   229  			}
   230  		}
   231  	}
   232  	return false
   233  }
   234  
   235  const (
   236  	// timeFormat defines a simple timestamp with millisecond granularity
   237  	timeFormat = "15:04:05.000"
   238  	// ChaosDuck is the well known name for the chaosduck.
   239  	ChaosDuck = "chaosduck"
   240  	// QueueProxy is the well known name for the queueproxy.
   241  	QueueProxy = "queueproxy"
   242  )
   243  
   244  // Names of well known containers that do not produce nicely formatted logs that
   245  // could be easily filtered and parsed by handleLine. Logs from these containers
   246  // are captured without filtering.
   247  var wellKnownContainers = sets.NewString(ChaosDuck, QueueProxy)
   248  
   249  func (s *logSource) handleLine(l []byte, pod string, _ string) {
   250  	// This holds the standard structure of our logs.
   251  	var line struct {
   252  		Level      string    `json:"severity"`
   253  		Timestamp  time.Time `json:"timestamp"`
   254  		Controller string    `json:"knative.dev/controller"`
   255  		Caller     string    `json:"caller"`
   256  		Key        string    `json:"knative.dev/key"`
   257  		Message    string    `json:"message"`
   258  		Error      string    `json:"error"`
   259  
   260  		// TODO(mattmoor): Parse out more context.
   261  	}
   262  	if err := json.Unmarshal(l, &line); err != nil {
   263  		// Ignore malformed lines.
   264  		return
   265  	}
   266  	if line.Key == "" {
   267  		return
   268  	}
   269  
   270  	s.m.RLock()
   271  	defer s.m.RUnlock()
   272  
   273  	for name, logf := range s.keys {
   274  		// TODO(mattmoor): Do a slightly smarter match.
   275  		if !strings.Contains(line.Key, "/"+name) {
   276  			continue
   277  		}
   278  
   279  		// We also get logs not from controllers (activator, autoscaler).
   280  		// So replace controller string in them with their callsite.
   281  		site := line.Controller
   282  		if site == "" {
   283  			site = line.Caller
   284  		}
   285  		func() {
   286  			defer func() {
   287  				if err := recover(); err != nil {
   288  					logf("Invalid log format for pod %s: %s", pod, string(l))
   289  				}
   290  			}()
   291  			// E 15:04:05.000 webhook-699b7b668d-9smk2 [route-controller] [default/testroute-xyz] this is my message
   292  			msg := fmt.Sprintf("%s %s %s [%s] [%s] %s",
   293  				strings.ToUpper(string(line.Level[0])),
   294  				line.Timestamp.Format(timeFormat),
   295  				pod,
   296  				site,
   297  				line.Key,
   298  				line.Message)
   299  
   300  			if line.Error != "" {
   301  				msg += " err=" + line.Error
   302  			}
   303  
   304  			logf(msg)
   305  		}()
   306  	}
   307  }
   308  
   309  // handleGenericLine prints the given logline to all active tests as it cannot be parsed
   310  // and/or doesn't contain any correlation data (like the chaosduck for example).
   311  func (s *logSource) handleGenericLine(l []byte, pod string, cn string) {
   312  	s.m.RLock()
   313  	defer s.m.RUnlock()
   314  
   315  	for _, logf := range s.keys {
   316  		// I 15:04:05.000 webhook-699b7b668d-9smk2 this is my message
   317  		logf("I %s %s %s %s", time.Now().Format(timeFormat), pod, cn, string(l))
   318  	}
   319  }