-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(egress): report the sidecar's own resource usage from its cgroup #1411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
03a0156
889182e
7ebc711
a4eb99c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // Copyright 2026 Alibaba Group Holding Ltd. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build linux | ||
|
|
||
| package telemetry | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // cgroupRoot is where the container's own cgroup is mounted. With a cgroup namespace — | ||
| // the default for containerd and CRI-O on cgroup v2 — this path is the container's cgroup | ||
| // root, so the values below describe the sidecar and not the node. Overridden in tests. | ||
| var cgroupRoot = "/sys/fs/cgroup" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Docker/cgroup-v1 hosts where the sidecar uses the host cgroup namespace, the Docker launcher I checked does not force a private cgroup namespace ( Useful? React with 👍 / 👎. |
||
|
|
||
| // processMemoryUsageBytes returns the sidecar's own memory usage, trying cgroup v2 before | ||
| // v1. The bool is false when neither layout is readable, which is a real possibility: a | ||
| // sandbox pod may run under a runtime that does not expose cgroupfs (see secure_runtime), | ||
| // and the caller must then skip the metric rather than publish a zero. | ||
| func processMemoryUsageBytes() (int64, bool) { | ||
| if data, err := os.ReadFile(filepath.Join(cgroupRoot, "memory.current")); err == nil { | ||
| if value, ok := cgroupSingleValueBytes(data); ok { | ||
| return value, true | ||
| } | ||
| } | ||
| // memory has its own v1 mount; unlike cpuacct it is not co-mounted with another | ||
| // controller in any layout worth supporting. | ||
| if data, err := os.ReadFile(filepath.Join(cgroupRoot, "memory", "memory.usage_in_bytes")); err == nil { | ||
| if value, ok := cgroupSingleValueBytes(data); ok { | ||
| return value, true | ||
| } | ||
| } | ||
| return 0, false | ||
| } | ||
|
|
||
| // processCPUTimeSeconds returns the CPU time the sidecar has consumed, cgroup v2 first. | ||
| // Cumulative on purpose: a counter of consumed seconds composes with rate() and does not | ||
| // depend on the exporter's sampling interval, unlike a sampled utilisation ratio. | ||
| func processCPUTimeSeconds() (float64, bool) { | ||
| if data, err := os.ReadFile(filepath.Join(cgroupRoot, "cpu.stat")); err == nil { | ||
| if seconds, ok := cgroupCPUSecondsFromStat(data); ok { | ||
| return seconds, true | ||
| } | ||
| } | ||
| // cgroup v1: cpuacct may be mounted on its own or co-mounted with cpu, which is the | ||
| // systemd default and what a container usually inherits. Trying only the first layout | ||
| // would report CPU as unavailable on a readable cgroupfs. | ||
| for _, dir := range []string{"cpuacct", "cpu,cpuacct"} { | ||
| if data, err := os.ReadFile(filepath.Join(cgroupRoot, dir, "cpuacct.usage")); err == nil { | ||
| if seconds, ok := cgroupCPUSecondsFromNanos(data); ok { | ||
| return seconds, true | ||
| } | ||
| } | ||
| } | ||
| return 0, false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // Copyright 2026 Alibaba Group Holding Ltd. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build linux | ||
|
|
||
| package telemetry | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| // withCgroupRoot points the readers at a fake cgroupfs for the duration of a test. | ||
| func withCgroupRoot(t *testing.T, files map[string]string) string { | ||
| t.Helper() | ||
| root := t.TempDir() | ||
| for name, content := range files { | ||
| path := filepath.Join(root, name) | ||
| if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | ||
| t.Fatalf("mkdir: %v", err) | ||
| } | ||
| if err := os.WriteFile(path, []byte(content), 0o644); err != nil { | ||
| t.Fatalf("write %s: %v", name, err) | ||
| } | ||
| } | ||
| previous := cgroupRoot | ||
| cgroupRoot = root | ||
| t.Cleanup(func() { cgroupRoot = previous }) | ||
| return root | ||
| } | ||
|
|
||
| func TestProcessMetricsReadCgroupV2(t *testing.T) { | ||
| withCgroupRoot(t, map[string]string{ | ||
| "memory.current": "8036352\n", | ||
| "cpu.stat": "usage_usec 1500000\nuser_usec 1000000\n", | ||
| }) | ||
|
|
||
| memory, ok := processMemoryUsageBytes() | ||
| if !ok || memory != 8036352 { | ||
| t.Errorf("memory = %d, %v; want 8036352, true", memory, ok) | ||
| } | ||
| seconds, ok := processCPUTimeSeconds() | ||
| if !ok || seconds != 1.5 { | ||
| t.Errorf("cpu = %v, %v; want 1.5, true", seconds, ok) | ||
| } | ||
| } | ||
|
|
||
| func TestProcessMetricsFallBackToCgroupV1(t *testing.T) { | ||
| withCgroupRoot(t, map[string]string{ | ||
| "memory/memory.usage_in_bytes": "4096\n", | ||
| "cpuacct/cpuacct.usage": "2500000000\n", | ||
| }) | ||
|
|
||
| memory, ok := processMemoryUsageBytes() | ||
| if !ok || memory != 4096 { | ||
| t.Errorf("memory = %d, %v; want 4096, true", memory, ok) | ||
| } | ||
| seconds, ok := processCPUTimeSeconds() | ||
| if !ok || seconds != 2.5 { | ||
| t.Errorf("cpu = %v, %v; want 2.5, true", seconds, ok) | ||
| } | ||
| } | ||
|
|
||
| // The systemd default on cgroup v1 co-mounts cpu and cpuacct, and a container inherits | ||
| // that layout: cpuacct.usage then lives under cpu,cpuacct/ and not cpuacct/. | ||
| func TestProcessCPUReadsCoMountedCgroupV1(t *testing.T) { | ||
| withCgroupRoot(t, map[string]string{ | ||
| "cpu,cpuacct/cpuacct.usage": "3000000000\n", | ||
| }) | ||
|
|
||
| seconds, ok := processCPUTimeSeconds() | ||
| if !ok || seconds != 3.0 { | ||
| t.Errorf("cpu = %v, %v; want 3, true", seconds, ok) | ||
| } | ||
| } | ||
|
|
||
| // A runtime that does not expose cgroupfs must yield no reading at all. Publishing zero | ||
| // would be indistinguishable from an idle sidecar. | ||
| func TestProcessMetricsUnavailableWithoutCgroupfs(t *testing.T) { | ||
| withCgroupRoot(t, nil) | ||
|
|
||
| if _, ok := processMemoryUsageBytes(); ok { | ||
| t.Error("memory reported available with no cgroup files") | ||
| } | ||
| if _, ok := processCPUTimeSeconds(); ok { | ||
| t.Error("cpu reported available with no cgroup files") | ||
| } | ||
| } | ||
|
|
||
| // End to end: with a readable cgroupfs the instruments are registered and observed; with | ||
| // none, they must be absent from the collection rather than present and zero. | ||
| func TestProcessMetricsRegistrationFollowsAvailability(t *testing.T) { | ||
| t.Run("registered when readable", func(t *testing.T) { | ||
| withCgroupRoot(t, map[string]string{ | ||
| "memory.current": "8036352\n", | ||
| "cpu.stat": "usage_usec 1500000\n", | ||
| }) | ||
|
|
||
| collected := collectEgressMetrics(t) | ||
|
|
||
| if got := collected["egress.process.memory.usage_bytes"]; got != float64(8036352) { | ||
| t.Errorf("memory metric = %v, want 8036352", got) | ||
| } | ||
| if got := collected["egress.process.cpu.time"]; got != 1.5 { | ||
| t.Errorf("cpu metric = %v, want 1.5", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("absent when unreadable", func(t *testing.T) { | ||
| withCgroupRoot(t, nil) | ||
|
|
||
| collected := collectEgressMetrics(t) | ||
|
|
||
| if _, present := collected["egress.process.memory.usage_bytes"]; present { | ||
| t.Error("memory metric registered without a cgroup source") | ||
| } | ||
| if _, present := collected["egress.process.cpu.time"]; present { | ||
| t.Error("cpu metric registered without a cgroup source") | ||
| } | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // Copyright 2026 Alibaba Group Holding Ltd. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build !linux | ||
|
|
||
| package telemetry | ||
|
|
||
| // cgroups are Linux-only. Both report unavailable so the instruments are never | ||
| // registered, mirroring the hostmetrics_stub split. | ||
| func processMemoryUsageBytes() (int64, bool) { return 0, false } | ||
|
|
||
| func processCPUTimeSeconds() (float64, bool) { return 0, false } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Copyright 2026 Alibaba Group Holding Ltd. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package telemetry | ||
|
|
||
| import ( | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
|
|
||
| // cgroupSingleValueBytes reads a cgroup file holding one integer: memory.current on v2, | ||
| // memory.usage_in_bytes on v1. Returns false for "max" and for anything unparseable, so a | ||
| // caller never mistakes an unset limit or a garbled read for a measurement. | ||
| func cgroupSingleValueBytes(data []byte) (int64, bool) { | ||
| field := strings.TrimSpace(string(data)) | ||
| if field == "" || field == "max" { | ||
| return 0, false | ||
| } | ||
| value, err := strconv.ParseInt(field, 10, 64) | ||
| if err != nil || value < 0 { | ||
| return 0, false | ||
| } | ||
| return value, true | ||
| } | ||
|
|
||
| // cgroupCPUSecondsFromStat pulls usage_usec out of a cgroup v2 cpu.stat. | ||
| func cgroupCPUSecondsFromStat(data []byte) (float64, bool) { | ||
| for _, line := range strings.Split(string(data), "\n") { | ||
| field, ok := strings.CutPrefix(strings.TrimSpace(line), "usage_usec ") | ||
| if !ok { | ||
| continue | ||
| } | ||
| micros, err := strconv.ParseInt(strings.TrimSpace(field), 10, 64) | ||
| if err != nil || micros < 0 { | ||
| return 0, false | ||
| } | ||
| return float64(micros) / 1e6, true | ||
| } | ||
| return 0, false | ||
| } | ||
|
|
||
| // cgroupCPUSecondsFromNanos converts a cgroup v1 cpuacct.usage (nanoseconds) to seconds. | ||
| func cgroupCPUSecondsFromNanos(data []byte) (float64, bool) { | ||
| nanos, ok := cgroupSingleValueBytes(data) | ||
| if !ok { | ||
| return 0, false | ||
| } | ||
| return float64(nanos) / 1e9, true | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.