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 }