Skip to content

feat(egress): report the sidecar's own resource usage from its cgroup - #1411

Open
ferponse wants to merge 4 commits into
opensandbox-group:mainfrom
ferponse:feat/egress-cgroup-metrics
Open

feat(egress): report the sidecar's own resource usage from its cgroup#1411
ferponse wants to merge 4 commits into
opensandbox-group:mainfrom
ferponse:feat/egress-cgroup-metrics

Conversation

@ferponse

Copy link
Copy Markdown
Contributor

Fixes #1409.

Problem

egress.system.memory.usage_bytes and egress.system.cpu.utilization come from gopsutil, i.e. /proc/meminfo and /proc/stat, which inside a container describe the node.

The descriptions do say "System", so the metrics are not lying — but the sidecar runs per sandbox, and the values are published with the per-sandbox attribute set:

obs.Observe(systemMemoryUsedBytes(), egressMetricOpt())   // carries sandbox_id

So with 30 sandboxes on a node you get 30 identical series that look per-sandbox. A "memory by sandbox" panel is a chart of one node number repeated 30 times, at 30× the cardinality, and it invites exactly the wrong conclusion.

(ingress has the same file, but as a single Deployment it yields one series, so it is harmless there. This is specific to the per-sandbox sidecar.)

Fix

Additive — nothing is removed or renamed, so no existing dashboard breaks. The system gauges are now documented as node-level, and two genuinely per-sandbox metrics are added, read from the sidecar's own cgroup:

Metric Source (v2 → v1 fallback)
egress.process.memory.usage_bytes memory.currentmemory/memory.usage_in_bytes
egress.process.cpu.time cpu.stat usage_useccpuacct/cpuacct.usage

Two deliberate choices:

  • CPU is a cumulative counter of seconds, not a ratio. A sampled utilisation figure depends on the exporter's interval, so it cannot be re-aggregated or compared across deployments configured differently. A counter composes with rate().
  • Registration is conditional on the files being readable. If cgroupfs is not exposed the instruments are never created, so the operator sees an absent series rather than a flat zero — which would be indistinguishable from an idle sidecar. This is not hypothetical for this project: a sandbox pod running under secure_runtime is exactly the case where the assumption might not hold.

Testing

go test ./... green across the egress module, go vet and gofmt clean, builds for linux/amd64 and darwin/arm64 (the !linux stub).

cgroupRoot is a package variable so tests can point it at a fake cgroupfs. Covered:

  • cgroup v2 layout read correctly
  • v1 fallback when v2 files are absent
  • no reading when neither exists
  • registration follows availability end to end — the instruments appear in a ManualReader collection when the files are there and are absent when they are not

Parsers are split into cgroup_parse.go with build-tag-free table tests, mirroring the existing meminfo_parse.go split. They reject max, empty content, unparseable and negative values, and usage_usec is not confused with user_usec — which is the kind of prefix bug that would otherwise report user time as total CPU.

Note on overlap

Touches pkg/telemetry/metrics.go, as do #1405 and #1410. Independent in substance; whichever merges first leaves the others needing a trivial rebase in the registration block.

egress.system.memory.usage_bytes and egress.system.cpu.utilization come from
gopsutil, so inside a container they describe the node. The sidecar runs per
sandbox, which means every sandbox on a node publishes the same node figure under
its own sandbox_id: N identical series that look per-sandbox and invite exactly
the wrong reading, at N times the cardinality.

Add egress.process.memory.usage_bytes and egress.process.cpu.time, read from the
sidecar's own cgroup (v2 memory.current and cpu.stat, falling back to v1
memory.usage_in_bytes and cpuacct.usage). Additive, so no existing dashboard
breaks; the system gauges are now documented as node-level rather than removed.

CPU is a cumulative counter of consumed seconds instead of a sampled ratio, so it
composes with rate() and does not depend on the exporter's interval.

Registration is conditional on the files being readable. A pod under a runtime
that does not expose cgroupfs gets no series rather than a flat zero, which would
be indistinguishable from an idle sidecar. cgroupRoot is a variable so the tests
can point it at a fake cgroupfs and cover v2, the v1 fallback, the unavailable
case, and that registration actually follows availability.

Fixes opensandbox-group#1409
@github-actions github-actions Bot added component/egress size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03a0156668

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/egress/pkg/telemetry/cgroup_linux.go Outdated
Comment thread components/egress/docs/opentelemetry.md
… docs/

Two review findings.

On cgroup v1 the systemd default co-mounts cpu and cpuacct, so cpuacct.usage sits
under cpu,cpuacct/ rather than cpuacct/ — the layout a container usually inherits.
Probing only the latter reported CPU as unavailable on a perfectly readable
cgroupfs, so egress.process.cpu.time was never registered there. Try both.

The metric documentation only existed in components/egress/docs, which nothing in
docs/ links to, so the published site would not have shown the new metrics or the
caveat about the node-level ones. Per AGENTS.md the operations-visible content
belongs in docs/, so the node-vs-sidecar distinction now lives in
docs/components/egress.md, whose Observability section previously named no metrics
at all, with a pointer to the component page for the full inventory.
@ferponse
ferponse requested a review from ninan-nn as a code owner July 28, 2026 16:32
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 889182e389

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/egress/pkg/telemetry/metrics.go
@ferponse

Copy link
Copy Markdown
Contributor Author

Both correct — fixed in 889182e.

cgroup v1 co-mount. You're right, and this would have been a silent hole rather than a visible failure: on a co-mounted v1 layout the file exists, is readable, and my code would have concluded "no cgroupfs" and never registered egress.process.cpu.time. Since the metric is supposed to be absent when the source is unreadable, nothing would have looked wrong. Now both cpuacct/cpuacct.usage and cpu,cpuacct/cpuacct.usage are probed, with a test for the co-mounted layout — and I checked it fails when the second path is removed.

I went with a candidate list rather than parsing /proc/self/mountinfo. Two known layouts are cheap to enumerate and cheap to test; mountinfo discovery adds a parser and its own failure modes for coverage of layouts I cannot point at. If you would rather have the general solution, say so and I will switch. memory keeps its single v1 path, since it is not co-mounted with another controller in any layout worth supporting.

Docs. Also right, and I had the rule backwards. docs/components/egress.md names no metrics at all today and nothing links to components/egress/docs/opentelemetry.md, so on the published site these metrics simply would not exist. The node-vs-sidecar distinction — the part that actually changes what an operator does — is now in docs/components/egress.md, with a pointer to the component page for the full inventory. I did not duplicate the whole table: one of the two would rot, and the caveat is the load-bearing half.

I applied the same fix to my other two open metric PRs (#1405, #1410) rather than waiting to be told twice.

Review pointed out that neither runtime sets OPENSANDBOX_EGRESS_SANDBOX_ID unless
the caller does, so sidecars from different sandboxes export identical attribute
sets and their series collide. That predates this branch and applies to every
egress metric, but it matters most here: colliding gauges look flat or flapping
rather than absent, which is the failure mode this branch was trying to avoid.

The env var is server-side plumbing, fixed in a separate PR. Documented here so
the caveat travels with the metrics that expose it.
@ferponse

Copy link
Copy Markdown
Contributor Author

The finding is correct: neither runtime sets OPENSANDBOX_EGRESS_SANDBOX_ID unless the caller supplies it, so sidecars from different sandboxes export identical attribute sets and their series collide.

It is not specific to this branch — it applies to every egress metric on main today, including the existing egress.system.* gauges. But your point that it matters more here is right: colliding gauges look flat or flapping rather than absent, which is the failure mode I was trying to avoid with the conditional registration.

The server-side fix is already open as #1402, which pins OPENSANDBOX_EGRESS_SANDBOX_ID from the real sandbox ID in both runtimes rather than trusting the request — for the same reason you raise, plus a spoofing angle: the env var is in ALLOWED_EGRESS_ENV_VARS, so a create request could otherwise report under another sandbox's identity.

I would rather not duplicate that plumbing here, since the two PRs would then conflict over the same helper. What I have done in 7ebc711 is document the dependency next to the metrics that expose it, so an operator deploying this without #1402 knows why their process series look wrong.

If you would prefer this PR to stand entirely on its own, the alternative is a resource attribute derived from something the sidecar already knows — hostname or pod IP — as a fallback identity when the env var is unset. Say the word and I will add it; I left it out because two competing identity attributes is its own kind of mess.

…metrics

# Conflicts:
#	components/egress/pkg/telemetry/metrics_test.go
#	docs/components/egress.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4eb99c107

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the actual cgroup path before reading stats

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 (server/opensandbox_server/services/docker/networking.py:431-435), so /sys/fs/cgroup is the controller mount root rather than this container's cgroup. The new v1 fallbacks then read root/controller metrics such as memory/memory.usage_in_bytes or cpu,cpuacct/cpuacct.usage, making egress.process.* report host-level usage or disappear instead of sidecar usage; derive the current cgroup path from /proc/self/cgroup/mountinfo before joining these files.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/egress documentation Improvements or additions to documentation size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

egress: system CPU/memory gauges report the node but are tagged per sandbox, so N sandboxes publish N identical series

1 participant