Repository: google/cadvisor

Issue:
Add `ContainerExitCode` to `info.EventData` in case of container deletion events
## Problem?
`cadvisor` has an `EventChannel` where currently 4 types of events are pushed.
Specifically a `EventType.EventContainerDeleteion` event is pushed when `manager` detects a container is deleted and it currently only captures the timestamp of the deletion event and name of the container.
However, since the container can be deleted due to many chaos scenarios, the current information is not sufficient enough to capture why the container is deleted. There's a specific case added for out of memory issues, however rather than accommodating all such scenarios individually, it will be a good idea to also include the exit code of the main process of the container. 

We have seen use cases where we want to emit a metric when a container is killed with a non-zero exit code and we use `cadvisor` as our main container observability tool. We tried using metrics from orchestrators (k8s) however they are not reliable in our environment. Subscribing to the `EventContainerDeletion` in the event channel looked like a good approach for us, but it lacks the exit code. We do have a work around but it might be better to include this info from the source itself.

Mostly all container runtime provides a way of getting the exit code for their exited containers like docker has `client.ContainerInspect`, containerd API also provide a way to get the exit status using the taskClient.

## Possible Solution
This can be achieved by adding a new field here
```
type EventData struct {
	// Information about an OOM kill event.
	OomKill *OomKillEventData `json:"oom,omitempty"`

	ContainerExitCode int8 `json:"conainer_exit_code,omitempty`
}
```
and then updating the logic here
https://github.com/google/cadvisor/blob/5adb1c3bb38b4c5d50b31f39faf3214a44ae479b/manager/manager.go#L1042-L1052
to

```
	contRef, err := cont.handler.ContainerReference()
	if err != nil {
		return err
	}

	contExitCode, err := cont.handler.GetExitCode() ## New method to the ContainerHandler interface
        if err != nil {
		return err
	}

	newEvent := &info.Event{
		ContainerName: contRef.Name,
		Timestamp:     time.Now(),
		EventType:     info.EventContainerDeletion,
		EventData: info.EventData{
			ContainerExitCode: contExitCode,
		},
	}
```

Then each container runtime can individually implement its way of getting the exit code.




Discussion:
@sambhav-jain-16: Hi @cwangVT,
Wanted to check if you have any thoughts on this issue? TIA@github-actions: This issue is stale because it has been open 90 days with no activity. This issue will be closed in 30 days unless new comments are made or the stale label is removed. To skip these checks, apply the "lifecycle/frozen" label.

Solution patch:
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 00000000..f468c055
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,30 @@
+name: Stale issues and pull requests
+
+on:
+  schedule:
+  - cron: "21 4 * * *"
+  workflow_dispatch:
+
+jobs:
+  stale:
+    permissions:
+      issues: write
+      pull-requests: write
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/stale@v10
+      with:
+        stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. This issue will be closed in 30 days unless new comments are made or the stale label is removed. To skip these checks, apply the "lifecycle/frozen" label.'
+        stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. This PR will be closed in 30 days unless new comments are made or the stale label is removed. To skip these checks, apply the "lifecycle/frozen" label.'
+        stale-issue-label: 'lifecycle/stale'
+        stale-pr-label: 'lifecycle/stale'
+        exempt-issue-labels: 'lifecycle/frozen'
+        exempt-pr-labels: 'lifecycle/frozen'
+        days-before-stale: 90
+        close-issue-message: 'This issue was automatically closed due to inactivity.'
+        close-pr-message: 'This pull request was automatically closed due to inactivity.'
+        days-before-issue-close: 30
+        days-before-pr-close: 30
+        remove-stale-when-updated: true
+        operations-per-run: 300
diff --git a/container/containerd/client.go b/container/containerd/client.go
index 2c41a2d5..208323d9 100644
--- a/container/containerd/client.go
+++ b/container/containerd/client.go
@@ -45,6 +45,7 @@ type client struct {
 type ContainerdClient interface {
 	LoadContainer(ctx context.Context, id string) (*containers.Container, error)
 	TaskPid(ctx context.Context, id string) (uint32, error)
+	LoadTaskProcess(ctx context.Context, id string) (*tasktypes.Process, error)
 	Version(ctx context.Context) (string, error)
 }
 
@@ -132,6 +133,17 @@ func (c *client) TaskPid(ctx context.Context, id string) (uint32, error) {
 	return response.Process.Pid, nil
 }
 
+func (c *client) LoadTaskProcess(ctx context.Context, id string) (*tasktypes.Process, error) {
+	response, err := c.taskService.Get(ctx, &tasksapi.GetRequest{
+		ContainerID: id,
+	})
+	if err != nil {
+		return nil, errgrpc.ToNative(err)
+	}
+
+	return response.Process, nil
+}
+
 func (c *client) Version(ctx context.Context) (string, error) {
 	response, err := c.versionService.Version(ctx, &emptypb.Empty{})
 	if err != nil {
diff --git a/container/containerd/factory.go b/container/containerd/factory.go
index 1688bd78..b600e19b 100644
--- a/container/containerd/factory.go
+++ b/container/containerd/factory.go
@@ -15,13 +15,13 @@
 package containerd
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"path"
 	"regexp"
 	"strings"
 
-	"golang.org/x/net/context"
 	"k8s.io/klog/v2"
 
 	"github.com/google/cadvisor/container"
diff --git a/container/containerd/grpc.go b/container/containerd/grpc.go
index 53210405..e73a9a17 100644
--- a/container/containerd/grpc.go
+++ b/container/containerd/grpc.go
@@ -16,9 +16,11 @@
 package containerd
 
 import (
-	"github.com/google/cadvisor/container/containerd/namespaces"
-	"golang.org/x/net/context"
+	"context"
+
 	"google.golang.org/grpc"
+
+	"github.com/google/cadvisor/container/containerd/namespaces"
 )
 
 type namespaceInterceptor struct {
diff --git a/container/containerd/handler.go b/container/containerd/handler.go
index d7f0864a..b793cf3c 100644
--- a/container/containerd/handler.go
+++ b/container/containerd/handler.go
@@ -16,6 +16,7 @@
 package containerd
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -25,7 +26,6 @@ import (
 	"github.com/containerd/errdefs"
 	"github.com/opencontainers/cgroups"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
-	"golang.org/x/net/context"
 
 	"github.com/google/cadvisor/container"
 	"github.com/google/cadvisor/container/common"
diff --git a/container/docker/docker.go b/container/docker/docker.go
index 42b908d1..7455c69e 100644
--- a/container/docker/docker.go
+++ b/container/docker/docker.go
@@ -16,6 +16,7 @@
 package docker
 
 import (
+	"context"
 	"fmt"
 	"regexp"
 	"strconv"
@@ -23,7 +24,6 @@ import (
 
 	dockerimage "github.com/docker/docker/api/types/image"
 	dockersystem "github.com/docker/docker/api/types/system"
-	"golang.org/x/net/context"
 
 	"github.com/google/cadvisor/container/docker/utils"
 	v1 "github.com/google/cadvisor/info/v1"
@@ -32,17 +32,14 @@ import (
 
 var dockerTimeout = 10 * time.Second
 
-func defaultContext() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), dockerTimeout)
-	return ctx
-}
-
 func SetTimeout(timeout time.Duration) {
 	dockerTimeout = timeout
 }
 
 func Status() (v1.DockerStatus, error) {
-	return StatusWithContext(defaultContext())
+	ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
+	defer cancel()
+	return StatusWithContext(ctx)
 }
 
 func StatusWithContext(ctx context.Context) (v1.DockerStatus, error) {
@@ -89,7 +86,9 @@ func Images() ([]v1.DockerImage, error) {
 	if err != nil {
 		return nil, fmt.Errorf("unable to communicate with docker daemon: %v", err)
 	}
-	summaries, err := client.ImageList(defaultContext(), dockerimage.ListOptions{All: false})
+	ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
+	defer cancel()
+	summaries, err := client.ImageList(ctx, dockerimage.ListOptions{All: false})
 	if err != nil {
 		return nil, err
 	}
@@ -135,7 +134,9 @@ func Info() (*dockersystem.Info, error) {
 		return nil, fmt.Errorf("unable to communicate with docker daemon: %v", err)
 	}
 
-	dockerInfo, err := client.Info(defaultContext())
+	ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
+	defer cancel()
+	dockerInfo, err := client.Info(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("failed to detect Docker info: %v", err)
 	}
@@ -155,7 +156,9 @@ func VersionString() (string, error) {
 	dockerVersion := "Unknown"
 	client, err := Client()
 	if err == nil {
-		version, err := client.ServerVersion(defaultContext())
+		ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
+		defer cancel()
+		version, err := client.ServerVersion(ctx)
 		if err == nil {
 			dockerVersion = version.Version
 		}
@@ -167,7 +170,9 @@ func APIVersionString() (string, error) {
 	apiVersion := "Unknown"
 	client, err := Client()
 	if err == nil {
-		version, err := client.ServerVersion(defaultContext())
+		ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
+		defer cancel()
+		version, err := client.ServerVersion(ctx)
 		if err == nil {
 			apiVersion = version.APIVersion
 		}
diff --git a/container/docker/factory.go b/container/docker/factory.go
index 37254540..fec0ea50 100644
--- a/container/docker/factory.go
+++ b/container/docker/factory.go
@@ -15,6 +15,7 @@
 package docker
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"regexp"
@@ -26,7 +27,6 @@ import (
 	"github.com/blang/semver/v4"
 	dockersystem "github.com/docker/docker/api/types/system"
 	dclient "github.com/docker/docker/client"
-	"golang.org/x/net/context"
 	"k8s.io/klog/v2"
 
 	"github.com/google/cadvisor/container"
diff --git a/container/docker/handler.go b/container/docker/handler.go
index 8fa422e5..dfa10e1b 100644
--- a/container/docker/handler.go
+++ b/container/docker/handler.go
@@ -16,6 +16,7 @@
 package docker
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -27,7 +28,6 @@ import (
 	dclient "github.com/docker/docker/client"
 	"github.com/opencontainers/cgroups"
 	"github.com/opencontainers/runtime-spec/specs-go"
-	"golang.org/x/net/context"
 
 	"github.com/google/cadvisor/container"
 	"github.com/google/cadvisor/container/common"
diff --git a/container/docker/plugin.go b/container/docker/plugin.go
index 07e471a7..a4a9e391 100644
--- a/container/docker/plugin.go
+++ b/container/docker/plugin.go
@@ -15,9 +15,9 @@
 package docker
 
 import (
+	"context"
 	"time"
 
-	"golang.org/x/net/context"
 	"k8s.io/klog/v2"
 
 	"github.com/google/cadvisor/container"
@@ -56,7 +56,8 @@ func retryDockerStatus() info.DockerStatus {
 	startupTimeout := dockerClientTimeout
 	maxTimeout := 4 * startupTimeout
 	for {
-		ctx, _ := context.WithTimeout(context.Background(), startupTimeout)
+		ctx, cancel := context.WithTimeout(context.Background(), startupTimeout)
+		defer cancel()
 		dockerStatus, err := StatusWithContext(ctx)
 		if err == nil {
 			return dockerStatus
diff --git a/container/docker/utils/docker.go b/container/docker/utils/docker.go
index 71913fed..db02bd05 100644
--- a/container/docker/utils/docker.go
+++ b/container/docker/utils/docker.go
@@ -23,6 +23,7 @@ import (
 
 	dockerimage "github.com/docker/docker/api/types/image"
 	dockersystem "github.com/docker/docker/api/types/system"
+
 	v1 "github.com/google/cadvisor/info/v1"
 )
 
diff --git a/container/factory.go b/container/factory.go
index dfe6de64..f5a3c90f 100644
--- a/container/factory.go
+++ b/container/factory.go
@@ -192,7 +192,6 @@ func RegisterPlugin(name string, plugin Plugin) error {
 	if _, found := plugins[name]; found {
 		return fmt.Errorf("Plugin %q was registered twice", name)
 	}
-	klog.V(4).Infof("Registered Plugin %q", name)
 	plugins[name] = plugin
 	return nil
 }
diff --git a/go.mod b/go.mod
index 50f1e135..d96bd6bd 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,6 @@ require (
 	github.com/prometheus/client_model v0.6.2
 	github.com/prometheus/common v0.64.0
 	github.com/stretchr/testify v1.10.0
-	golang.org/x/net v0.40.0
 	golang.org/x/sys v0.33.0
 	google.golang.org/grpc v1.72.2
 	google.golang.org/protobuf v1.36.6
@@ -81,6 +80,7 @@ require (
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
 	go.opentelemetry.io/otel/metric v1.36.0 // indirect
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	golang.org/x/net v0.40.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
diff --git a/info/v2/container.go b/info/v2/container.go
index f0824027..7845fe4c 100644
--- a/info/v2/container.go
+++ b/info/v2/container.go
@@ -200,6 +200,8 @@ type Percentiles struct {
 	Ninety uint64 `json:"ninety"`
 	// 95th percentile over the collected sample.
 	NinetyFive uint64 `json:"ninetyfive"`
+	// Number of samples used to calculate these percentiles.
+	Count uint64 `json:"count"`
 }
 
 type Usage struct {
diff --git a/machine/info.go b/machine/info.go
index 76d2272f..6e91095c 100644
--- a/machine/info.go
+++ b/machine/info.go
@@ -15,7 +15,6 @@
 package machine
 
 import (
-	"bytes"
 	"flag"
 	"os"
 	"path/filepath"
@@ -169,10 +168,8 @@ func ContainerOsVersion() string {
 
 func KernelVersion() string {
 	uname := &unix.Utsname{}
-
 	if err := unix.Uname(uname); err != nil {
 		return "Unknown"
 	}
-
-	return string(uname.Release[:bytes.IndexByte(uname.Release[:], 0)])
+	return unix.ByteSliceToString(uname.Release[:])
 }
diff --git a/machine/machine.go b/machine/machine.go
index 0ba16161..a3699a14 100644
--- a/machine/machine.go
+++ b/machine/machine.go
@@ -257,7 +257,7 @@ func getMachineArch() string {
 		klog.Errorf("Cannot get machine architecture, err: %v", err)
 		return ""
 	}
-	return string(uname.Machine[:])
+	return unix.ByteSliceToString(uname.Machine[:])
 }
 
 // arm32 changes
diff --git a/machine/operatingsystem_unix.go b/machine/operatingsystem_unix.go
index 91c355bc..92e409d8 100644
--- a/machine/operatingsystem_unix.go
+++ b/machine/operatingsystem_unix.go
@@ -19,10 +19,11 @@ package machine
 import (
 	"fmt"
 	"os"
-	"os/exec"
 	"regexp"
 	"runtime"
 	"strings"
+
+	"golang.org/x/sys/unix"
 )
 
 var rex = regexp.MustCompile("(PRETTY_NAME)=(.*)")
@@ -30,12 +31,12 @@ var rex = regexp.MustCompile("(PRETTY_NAME)=(.*)")
 // getOperatingSystem gets the name of the current operating system.
 func getOperatingSystem() (string, error) {
 	if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
-		cmd := exec.Command("uname", "-s")
-		osName, err := cmd.Output()
+		uname := unix.Utsname{}
+		err := unix.Uname(&uname)
 		if err != nil {
 			return "", err
 		}
-		return string(osName), nil
+		return unix.ByteSliceToString(uname.Sysname[:]), nil
 	}
 	bytes, err := os.ReadFile("/etc/os-release")
 	if err != nil && os.IsNotExist(err) {
diff --git a/summary/percentiles.go b/summary/percentiles.go
index 8ce706ad..bca056e9 100644
--- a/summary/percentiles.go
+++ b/summary/percentiles.go
@@ -109,6 +109,7 @@ func (r *resource) AddSample(val uint64) {
 		Fifty:      val,
 		Ninety:     val,
 		NinetyFive: val,
+		Count:      1,
 	}
 	r.Add(sample)
 }
@@ -121,6 +122,8 @@ func (r *resource) GetAllPercentiles() info.Percentiles {
 	p.Fifty = r.samples.GetPercentile(0.5)
 	p.Ninety = r.samples.GetPercentile(0.9)
 	p.NinetyFive = r.samples.GetPercentile(0.95)
+	// len(samples) is equal to count stored in mean.
+	p.Count = r.mean.count
 	p.Present = true
 	return p
 }



Test patch:
diff --git a/container/containerd/client_test.go b/container/containerd/client_test.go
index eaaab0e4..a4af2ee0 100644
--- a/container/containerd/client_test.go
+++ b/container/containerd/client_test.go
@@ -18,12 +18,14 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/containerd/containerd/api/types/task"
 	"github.com/google/cadvisor/container/containerd/containers"
 )
 
 type containerdClientMock struct {
 	cntrs     map[string]*containers.Container
 	returnErr error
+	tasks     map[string]*task.Process
 }
 
 func (c *containerdClientMock) LoadContainer(ctx context.Context, id string) (*containers.Container, error) {
@@ -45,9 +47,27 @@ func (c *containerdClientMock) TaskPid(ctx context.Context, id string) (uint32,
 	return 2389, nil
 }
 
+func (c *containerdClientMock) LoadTaskProcess(ctx context.Context, id string) (*task.Process, error) {
+	if c.returnErr != nil {
+		return nil, c.returnErr
+	}
+	task, ok := c.tasks[id]
+	if !ok {
+		return nil, fmt.Errorf("unable to find task for container %q", id)
+	}
+	return task, nil
+}
+
 func mockcontainerdClient(cntrs map[string]*containers.Container, returnErr error) ContainerdClient {
+	tasks := make(map[string]*task.Process)
+
+	for _, cntr := range cntrs {
+		tasks[cntr.ID] = &task.Process{}
+	}
+
 	return &containerdClientMock{
 		cntrs:     cntrs,
 		returnErr: returnErr,
+		tasks:     tasks,
 	}
 }
diff --git a/integration/tests/api/machinestats_test.go b/integration/tests/api/machinestats_test.go
index a18b4e41..fd94b654 100644
--- a/integration/tests/api/machinestats_test.go
+++ b/integration/tests/api/machinestats_test.go
@@ -21,8 +21,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
-	"github.com/google/cadvisor/integration/framework"
 	"github.com/opencontainers/cgroups"
+
+	"github.com/google/cadvisor/integration/framework"
 )
 
 func TestMachineStatsIsReturned(t *testing.T) {
diff --git a/resctrl/intel/manager_test.go b/resctrl/intel/manager_test.go
index 504e25b2..c7008e92 100644
--- a/resctrl/intel/manager_test.go
+++ b/resctrl/intel/manager_test.go
@@ -21,8 +21,9 @@ import (
 	"os"
 	"testing"
 
-	"github.com/google/cadvisor/resctrl"
 	"github.com/stretchr/testify/assert"
+
+	"github.com/google/cadvisor/resctrl"
 )
 
 func TestNewManager(t *testing.T) {
diff --git a/summary/percentiles_test.go b/summary/percentiles_test.go
index 4dbe3665..b4cf6206 100644
--- a/summary/percentiles_test.go
+++ b/summary/percentiles_test.go
@@ -84,6 +84,8 @@ func TestAggregates(t *testing.T) {
 		Fifty:      1000,
 		Ninety:     1000,
 		NinetyFive: 1000,
+		// Since cpu is calculated between samples, we lose 1 sample.
+		Count: N - 2,
 	}
 	if usage.Cpu != cpuExpected {
 		t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected)
@@ -95,6 +97,7 @@ func TestAggregates(t *testing.T) {
 		Fifty:      50 * 1024,
 		Ninety:     90 * 1024,
 		NinetyFive: 95 * 1024,
+		Count:      N - 1,
 	}
 	if usage.Memory != memExpected {
 		t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected)
@@ -133,6 +136,8 @@ func TestSamplesCloseInTimeIgnored(t *testing.T) {
 		Fifty:      1000,
 		Ninety:     1000,
 		NinetyFive: 1000,
+		// Since cpu is calculated between samples, we lose 1 sample.
+		Count: N - 2,
 	}
 	if usage.Cpu != cpuExpected {
 		t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected)
@@ -144,6 +149,7 @@ func TestSamplesCloseInTimeIgnored(t *testing.T) {
 		Fifty:      50 * 1024,
 		Ninety:     90 * 1024,
 		NinetyFive: 95 * 1024,
+		Count:      N - 1,
 	}
 	if usage.Memory != memExpected {
 		t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected)
@@ -184,6 +190,8 @@ func TestDerivedStats(t *testing.T) {
 		Fifty:      50 * Nanosecond,
 		Ninety:     90 * Nanosecond,
 		NinetyFive: 95 * Nanosecond,
+		// GetDerivedPercentiles calculates directly from samples, hence we don't lose any samples.
+		Count: N - 1,
 	}
 	if usage.Cpu != cpuExpected {
 		t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected)
@@ -195,6 +203,7 @@ func TestDerivedStats(t *testing.T) {
 		Fifty:      50 * 1024,
 		Ninety:     90 * 1024,
 		NinetyFive: 95 * 1024,
+		Count:      N - 1,
 	}
 	if usage.Memory != memExpected {
 		t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected)