github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/gadget-collection/gadgets/traceloop/gadget.go (about) 1 // Copyright 2019-2021 The Inspektor Gadget authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package traceloop 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "strings" 22 "sync" 23 24 log "github.com/sirupsen/logrus" 25 "sigs.k8s.io/controller-runtime/pkg/client" 26 27 "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-collection/gadgets" 28 "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/traceloop/types" 29 30 tracelooptracer "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/traceloop/tracer" 31 32 gadgetv1alpha1 "github.com/inspektor-gadget/inspektor-gadget/pkg/apis/gadget/v1alpha1" 33 containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" 34 ) 35 36 type containerSlim struct { 37 mntnsid uint64 38 detached bool 39 } 40 41 type Trace struct { 42 client client.Client 43 helpers gadgets.GadgetHelpers 44 45 started bool 46 47 containerIDs map[string]*containerSlim 48 49 trace *gadgetv1alpha1.Trace 50 } 51 52 type TraceFactory struct { 53 gadgets.BaseFactory 54 } 55 56 type traceSingleton struct { 57 sync.Mutex 58 59 tracer *tracelooptracer.Tracer 60 users int 61 } 62 63 var traceUnique traceSingleton 64 65 func NewFactory() gadgets.TraceFactory { 66 return &TraceFactory{ 67 BaseFactory: gadgets.BaseFactory{DeleteTrace: deleteTrace}, 68 } 69 } 70 71 func (f *TraceFactory) Description() string { 72 return `The traceloop gadget traces system calls in a similar way to strace but with 73 some differences: 74 75 * traceloop uses eBPF instead of ptrace 76 * traceloop's tracing granularity is the container instead of a process 77 * traceloop's traces are recorded in a fast, in-memory, overwritable ring 78 buffer like a flight recorder. The tracing could be permanently enabled and 79 inspected in case of crash. 80 ` 81 } 82 83 func (f *TraceFactory) OutputModesSupported() map[gadgetv1alpha1.TraceOutputMode]struct{} { 84 return map[gadgetv1alpha1.TraceOutputMode]struct{}{ 85 gadgetv1alpha1.TraceOutputModeStatus: {}, 86 gadgetv1alpha1.TraceOutputModeStream: {}, 87 } 88 } 89 90 func deleteTrace(name string, t interface{}) { 91 trace := t.(*Trace) 92 if trace.started { 93 traceUnique.Lock() 94 defer traceUnique.Unlock() 95 96 traceUnique.users-- 97 if traceUnique.users == 0 { 98 trace.helpers.Unsubscribe(genPubSubKey(name)) 99 100 traceUnique.tracer.Stop() 101 102 traceUnique.tracer = nil 103 } 104 } 105 } 106 107 func (f *TraceFactory) Operations() map[gadgetv1alpha1.Operation]gadgets.TraceOperation { 108 n := func() interface{} { 109 return &Trace{ 110 client: f.Client, 111 helpers: f.Helpers, 112 } 113 } 114 115 return map[gadgetv1alpha1.Operation]gadgets.TraceOperation{ 116 gadgetv1alpha1.OperationStart: { 117 Doc: "Start traceloop", 118 Operation: func(name string, trace *gadgetv1alpha1.Trace) { 119 f.LookupOrCreate(name, n).(*Trace).Start(trace) 120 }, 121 }, 122 gadgetv1alpha1.OperationCollect: { 123 Doc: "Collect traceloop", 124 Operation: func(name string, trace *gadgetv1alpha1.Trace) { 125 // To overcome Status.Output character limit, we decided to create a 126 // Stream Trace CRD each time we do the collect operation. 127 // Nonetheless, this Trace CRD will use a previously created Trace. 128 // To do so, we use the Parameters["name"] which will contain the name 129 // of the long lived Trace CRD, thus we will be able to get the Trace 130 // and so all the mntNsIDs associated to it. 131 t, err := f.Lookup(trace.Spec.Parameters["name"]) 132 if err != nil { 133 trace.Status.OperationError = fmt.Sprintf("no global trace with name %q: %s", name, err) 134 135 return 136 } 137 t.(*Trace).Collect(trace) 138 }, 139 }, 140 gadgetv1alpha1.OperationDelete: { 141 Doc: "Delete a perf ring buffer owned by traceloop", 142 Operation: func(name string, trace *gadgetv1alpha1.Trace) { 143 // To overcome Status.Output character limit, we decided to create a 144 // Stream Trace CRD each time we do the collect operation. 145 // Nonetheless, this Trace CRD will use a previously created Trace. 146 // To do so, we use the Parameters["name"] which will contain the name 147 // of the long lived Trace CRD, thus we will be able to get the Trace 148 // and so all the mntNsIDs associated to it. 149 t, err := f.Lookup(trace.Spec.Parameters["name"]) 150 if err != nil { 151 trace.Status.OperationError = fmt.Sprintf("no global trace with name %q: %s", name, err) 152 153 return 154 } 155 t.(*Trace).Delete(trace) 156 }, 157 }, 158 gadgetv1alpha1.OperationStop: { 159 Doc: "Stop traceloop", 160 Operation: func(name string, trace *gadgetv1alpha1.Trace) { 161 f.LookupOrCreate(name, n).(*Trace).Stop(trace) 162 }, 163 }, 164 } 165 } 166 167 type pubSubKey string 168 169 func genPubSubKey(name string) pubSubKey { 170 return pubSubKey(fmt.Sprintf("gadget/traceloop/%s", name)) 171 } 172 173 func (t *Trace) Start(trace *gadgetv1alpha1.Trace) { 174 if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus { 175 trace.Status.OperationError = fmt.Sprintf("\"start\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode) 176 177 return 178 } 179 180 if t.started { 181 trace.Status.State = gadgetv1alpha1.TraceStateStarted 182 return 183 } 184 185 // Having this backlink is mandatory for delete operation. 186 t.trace = trace 187 188 // The output will contain an array of types.TraceloopInfo. 189 // So, to avoid problems, we initialize it to be a JSON array. 190 trace.Status.Output = "[]" 191 t.containerIDs = make(map[string]*containerSlim, 0) 192 193 genKey := func(container *containercollection.Container) string { 194 return container.K8s.Namespace + "/" + container.K8s.PodName 195 } 196 197 attachContainerFunc := func(container *containercollection.Container) error { 198 containerID := container.Runtime.ContainerID 199 mntNsID := container.Mntns 200 key := genKey(container) 201 202 traceUnique.Lock() 203 err := traceUnique.tracer.Attach(containerID, mntNsID) 204 traceUnique.Unlock() 205 if err != nil { 206 log.Errorf("failed to attach tracer: %s", err) 207 208 return err 209 } 210 211 t.containerIDs[containerID] = &containerSlim{ 212 mntnsid: mntNsID, 213 } 214 log.Debugf("tracer attached for %q (%d)", key, mntNsID) 215 216 var infos []types.TraceloopInfo 217 err = json.Unmarshal([]byte(trace.Status.Output), &infos) 218 if err != nil { 219 log.Errorf("failed to unmarshal output: %s", err) 220 221 return err 222 } 223 224 infos = append(infos, types.TraceloopInfo{ 225 Namespace: container.K8s.Namespace, 226 Podname: container.K8s.PodName, 227 Containername: container.K8s.ContainerName, 228 ContainerID: containerID, 229 }) 230 231 output, err := json.Marshal(infos) 232 if err != nil { 233 log.Errorf("failed to marshal infos: %s", err) 234 235 return err 236 } 237 238 traceBeforePatch := trace.DeepCopy() 239 trace.Status.Output = string(output) 240 patch := client.MergeFrom(traceBeforePatch) 241 242 // The surrounding function can be called from any context. 243 // So, we need to manually patch the trace CRD to have our modifications be 244 // taken into account. 245 err = t.client.Status().Patch(context.TODO(), trace, patch) 246 if err != nil { 247 log.Errorf("Failed to patch trace %q output: %s", trace.Name, err) 248 249 return err 250 } 251 252 return nil 253 } 254 255 detachContainerFunc := func(container *containercollection.Container) { 256 mntNsID := container.Mntns 257 key := genKey(container) 258 259 traceUnique.Lock() 260 err := traceUnique.tracer.Detach(mntNsID) 261 traceUnique.Unlock() 262 if err != nil { 263 log.Errorf("failed to detach tracer: %s", err) 264 265 return 266 } 267 268 _, ok := t.containerIDs[container.Runtime.ContainerID] 269 if ok { 270 t.containerIDs[container.Runtime.ContainerID].detached = true 271 } else { 272 log.Errorf("trace does not know about container with ID %q", container.Runtime.ContainerID) 273 274 return 275 } 276 277 log.Debugf("tracer detached for %q (%d)", key, mntNsID) 278 } 279 280 containerEventCallback := func(event containercollection.PubSubEvent) { 281 switch event.Type { 282 case containercollection.EventTypeAddContainer: 283 attachContainerFunc(event.Container) 284 case containercollection.EventTypeRemoveContainer: 285 detachContainerFunc(event.Container) 286 } 287 } 288 289 traceUnique.Lock() 290 if traceUnique.tracer == nil { 291 var syscallFilters string 292 var err error 293 294 filters, ok := trace.Spec.Parameters["syscall-filters"] 295 if ok { 296 syscallFilters = filters 297 } 298 299 traceUnique.tracer, err = tracelooptracer.NewTracer(t.helpers, strings.Split(syscallFilters, ",")) 300 if err != nil { 301 traceUnique.Unlock() 302 303 trace.Status.OperationError = fmt.Sprintf("Failed to start traceloop tracer: %s", err) 304 305 return 306 } 307 } 308 traceUnique.users++ 309 traceUnique.Unlock() 310 311 existingContainers := t.helpers.Subscribe( 312 genPubSubKey(trace.ObjectMeta.Namespace+"/"+trace.ObjectMeta.Name), 313 *gadgets.ContainerSelectorFromContainerFilter(trace.Spec.Filter), 314 containerEventCallback, 315 ) 316 317 for _, c := range existingContainers { 318 err := attachContainerFunc(c) 319 if err != nil { 320 log.Warnf("couldn't attach BPF program: %s", err) 321 322 continue 323 } 324 } 325 t.started = true 326 327 trace.Status.State = gadgetv1alpha1.TraceStateStarted 328 } 329 330 func (t *Trace) Collect(trace *gadgetv1alpha1.Trace) { 331 if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStream { 332 trace.Status.OperationError = fmt.Sprintf("\"collect\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStream, trace.Spec.OutputMode) 333 334 return 335 } 336 337 traceUnique.Lock() 338 if traceUnique.tracer == nil { 339 traceUnique.Unlock() 340 341 trace.Status.OperationError = "Traceloop tracer is nil" 342 343 return 344 } 345 traceUnique.Unlock() 346 347 containerID := trace.Spec.Parameters["containerID"] 348 _, ok := t.containerIDs[containerID] 349 if !ok { 350 ids := make([]string, len(t.containerIDs)) 351 i := 0 352 for id := range t.containerIDs { 353 ids[i] = id 354 i++ 355 } 356 357 trace.Status.OperationError = fmt.Sprintf("%q is not a valid ID for this trace, valid IDs are: %v", containerID, strings.Join(ids, ",")) 358 359 return 360 } 361 362 traceUnique.Lock() 363 events, err := traceUnique.tracer.Read(containerID) 364 traceUnique.Unlock() 365 if err != nil { 366 trace.Status.OperationError = fmt.Sprintf("Failed to read perf buffer: %s", err) 367 368 return 369 } 370 371 traceName := gadgets.TraceName(trace.ObjectMeta.Namespace, trace.ObjectMeta.Name) 372 r, err := json.Marshal(events) 373 if err != nil { 374 log.Warnf("Gadget %s: error marshaling event: %s", trace.Spec.Gadget, err) 375 return 376 } 377 // HACK Traceloop is really particular as it cannot use Status output because 378 // the size is limited. 379 // Also, if we send each event as a line, we will only be able to get the last 380 // 100 (or 250) events from the CLI due to this code: 381 // https://github.com/inspektor-gadget/inspektor-gadget/blob/9c7b6a126d82b54262ffdc5709d7c92480002830/pkg/gadgettracermanager/stream/stream.go#L24 382 // https://github.com/inspektor-gadget/inspektor-gadget/blob/9c7b6a126d82b54262ffdc5709d7c92480002830/pkg/gadgettracermanager/stream/stream.go#L95-L100 383 // To overcome this limitation, we just send all events as one big line. 384 // Then, the CLI receives this big line, parses it and prints each event. 385 // A proper solution would be to develop a specific "output" (neither status 386 // nor stream) for traceloop, this is let as TODO. 387 t.helpers.PublishEvent(traceName, string(r)) 388 389 trace.Status.State = gadgetv1alpha1.TraceStateCompleted 390 } 391 392 func (t *Trace) Delete(trace *gadgetv1alpha1.Trace) { 393 if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus { 394 trace.Status.OperationError = fmt.Sprintf("\"delete\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode) 395 396 return 397 } 398 399 containerID := trace.Spec.Parameters["containerID"] 400 container, ok := t.containerIDs[containerID] 401 if !ok { 402 ids := make([]string, len(t.containerIDs)) 403 i := 0 404 for id := range t.containerIDs { 405 ids[i] = id 406 i++ 407 } 408 409 trace.Status.OperationError = fmt.Sprintf("%q is not a valid ID for this trace, valid IDs are: %v", containerID, strings.Join(ids, ",")) 410 411 return 412 } 413 414 traceUnique.Lock() 415 if traceUnique.tracer == nil { 416 traceUnique.Unlock() 417 418 trace.Status.OperationError = "Traceloop tracer is nil" 419 420 return 421 } 422 423 // First, we need to detach the perf buffer. 424 // We do not check the returned error because if the container was deleted it 425 // was already detached. 426 if !container.detached { 427 _ = traceUnique.tracer.Detach(container.mntnsid) 428 } 429 430 // Then we can remove it. 431 err := traceUnique.tracer.Delete(containerID) 432 traceUnique.Unlock() 433 if err != nil { 434 trace.Status.OperationError = fmt.Sprintf("Failed to delete perf buffer: %s", err) 435 436 return 437 } 438 439 // We can now remove containerID from the map. 440 delete(t.containerIDs, containerID) 441 442 // Finally, we need to update the trace output. 443 var infos []types.TraceloopInfo 444 err = json.Unmarshal([]byte(t.trace.Status.Output), &infos) 445 if err != nil { 446 trace.Status.OperationError = fmt.Sprintf("failed to unmarshal output: %s", err) 447 448 return 449 } 450 451 newInfos := make([]types.TraceloopInfo, len(infos)-1) 452 i := 0 453 for _, info := range infos { 454 // We copy all the current information except the one corresponding to the 455 // container we removed. 456 if info.ContainerID == containerID { 457 continue 458 } 459 460 newInfos[i] = info 461 i++ 462 } 463 464 output, err := json.Marshal(newInfos) 465 if err != nil { 466 trace.Status.OperationError = fmt.Sprintf("failed to marshal new infos: %s", err) 467 468 return 469 } 470 471 traceBeforePatch := t.trace.DeepCopy() 472 t.trace.Status.Output = string(output) 473 patch := client.MergeFrom(traceBeforePatch) 474 475 // We also need to manually patch the trace CRD to have our modifications be 476 // taken into account. 477 err = t.client.Status().Patch(context.TODO(), t.trace, patch) 478 if err != nil { 479 log.Errorf("failed to patch trace %q output: %s", trace.Name, err) 480 481 return 482 } 483 484 trace.Status.State = gadgetv1alpha1.TraceStateCompleted 485 } 486 487 func (t *Trace) Stop(trace *gadgetv1alpha1.Trace) { 488 if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus { 489 trace.Status.OperationError = fmt.Sprintf("\"stop\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode) 490 491 return 492 } 493 494 if !t.started { 495 trace.Status.OperationError = "Not started" 496 return 497 } 498 499 t.helpers.Unsubscribe(genPubSubKey(trace.ObjectMeta.Namespace + "/" + trace.ObjectMeta.Name)) 500 501 traceUnique.Lock() 502 traceUnique.users-- 503 if traceUnique.users == 0 { 504 traceUnique.tracer.Stop() 505 506 traceUnique.tracer = nil 507 } 508 traceUnique.Unlock() 509 510 t.started = false 511 512 trace.Status.State = gadgetv1alpha1.TraceStateStopped 513 }