Repository navigation
fix(agents): container CPU follows the load and memory percent is no longer 100x too high - #5076
Merged
Merged
Conversation
…it is a percent The Docker, Podman and Docker Swarm agents sent container.cpu.utilization as the docker_stats receiver computes it. Since collector 0.150.0 that receiver's Docker client asks the daemon for a one-shot stats sample, which carries no previous reading, so it divides all the CPU a container used since it started by all the CPU the host used since it booted: a flat line near zero that never follows the load. The agents run 0.161.0 (0.154.0 from June 2026 on, 0.118.0 before); Podman's Docker-compatible API never sends a previous reading, so Podman hosts always had it. Reproduced on a real Docker 29.8.1 daemon with collector 0.161.0: a container held to a quarter of a core read 0.014 while container.cpu.usage.total rose 25% of a core per second. The receiver's own figure is now off, and the agents build it from container.cpu.usage.total: copied, turned into the CPU time used since the previous scrape (cumulative_to_delta), divided by the seconds between the two scrapes (delta_to_rate) and scaled to percent of one core, unit "%". 100% is one core busy for the whole 30-second interval, the docker stats figure, and it always agrees with the rate of container.cpu.usage.total. Checked against the same daemon: 25.0% for a quarter core, 150.0% for one and a half, 0 / 30 / 100 / 69% for a container busy 10 s out of every 20, and no point (rather than a negative one) after a container restart. container.memory.percent was right in value (0-100 of the memory limit) but labelled "1", the unit of a 0-1 fraction, which OneUptime renders times 100: it now carries "%". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the real collector ContainerAgentCpuUtilization.test.js holds the Docker, Podman and Docker Swarm configs to the fix: the receiver's own CPU percent off and container.cpu.usage.total on, the copy / cumulative_to_delta / delta_to_rate / scale processors exactly as written and identical in all three, in order after memory_limiter in the metrics pipeline only, under the 0.161.0 names rather than the deprecated aliases, and the arithmetic (10^9 ns of CPU per second is 100%). container-agent-cpu-utilization-behaviour.sh runs them in the collector the agents are built FROM: six 30-second scrapes of two containers read 25 / 150 / 0 / nothing after a restart / 10% and 1% throughout, the counter and the memory percent go on unchanged but for "%"; then each agent's whole metrics pipeline runs against the machine's Docker daemon and a container held to half a core reads 50% (49.8-50.2 here), equal to the rate of container.cpu.usage.total, with memory percent equal to usage over limit. The Ops Config Test workflow runs it after the kubelet CPU behaviour test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ubernetes CPU charts scale once The docker_stats receiver (Docker, Podman and Docker Swarm agents) and podman_stats send container.cpu.utilization / container.memory.percent (container.cpu.percent for podman_stats) as 0-100 but declare the unit "1", a 0-1 fraction. OneUptime read every one of them 100 times too high: the metrics explorer, dashboards and the Docker/Podman/Swarm chart tabs (ValueFormatter and the "1" -> "%" chart conversion), generic metric monitors' thresholds, and the container rows ingest mirrors (cpuValueToPercent multiplied every "1"). The customer's traefik container, 4.36% of its memory limit, read 436.64%. Ingest now records the unit those numbers are in: a docker_stats or podman_stats percent declared "1" - told apart by its instrumentation scope, so the Kubernetes agent's container.cpu.utilization (cores, declared "1") is left alone - goes into the metric catalog as "%", and the container snapshots read it as "%" (ContainerStatsPercentUnit). The values are stored as sent: they were right, so a project's stored memory percents read right again once its next Docker or Podman batch has set the unit. The fixed agents send "%" themselves. One metric name, one unit per project: a project running the Kubernetes agent and a Docker agent gets container.cpu.utilization from both, cores declared "1" and a percent declared "%". The catalog keeps the "%" a container stats receiver set rather than flipping (and writing) on every other batch. The same flaw on the Kubernetes pages: their CPU charts (pods, containers, nodes, workloads) take cores declared "1" with the legend unit "%" and a transform that divides by the node's allocatable cores and multiplies by 100 - after the fetch had already converted "1" to "%", times 100. A pod using 1.25% of a 4-core node was drawn at 125%. A query with its own transformValue now gets its values as stored (MetricResultUnitConverter). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Swarm task CPU tooltips named no scale on purpose: ingest multiplied docker_stats' percent by 100 a second time on its way to the Task row, so "100% is one core" would have been false. The row now holds the percent as docker_stats sent it, and both texts (the task page's CPU field and the Tasks list's CPU column) say what 100% is, as the Docker and Podman host pages do. Renamed in place in all 17 locales and translated in all 16 (Japanese from its existing translation). DockerSwarmMetricTooltips pinned the old rule by reading cpuValueToPercent; it now reads that the metric loop gives the Task buffer the unit ContainerStatsPercentUnit settled, that a "%" is taken as it is, and that both texts name the scale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to do when it is a flat line
The Docker, Podman and Docker Swarm monitor pages describe
container.cpu.utilization as what it now is: the docker stats / podman
stats figure averaged over the agent's 30-second interval, 100% being one
core. Their "A CPU alert never fires" entries say what a flat line near
zero means (an older agent) and link to the upgrade.
The three agent guides get a troubleshooting entry, "Container CPU is a
flat line near zero" ("Task CPU" for Swarm): why the older agent's figure
is the container's lifetime CPU over the host's (since collector 0.150.0,
and always on Podman), that upgrading fixes it from the second reading,
and that stored readings stay as they were. The Swarm entry says no
warning sign shows (its version is the collector's, which did not change)
and to download the files again.
All 17 languages, line for line; anchors localized with
docs:localize-anchors. The agent READMEs listed container.cpu.percent,
which docker_stats never sends: they name container.cpu.utilization and
say how it is computed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… takes it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… previous report The same flaw as the container agents' CPU percent, in the host agent: each process's cpuPercent came from gopsutil's Process.CPUPercent, which divides all the CPU the process has used since it started by its age - a lifetime average that barely moves and does not follow the load (a process busy for its first second and idle since still reads 34%). The agent now keeps each process's CPU time (user plus system) from the previous 30-second report, keyed by PID and start time so a reused PID is not compared with another process, and reports the CPU used between the two as a percent of one core, as top counts it. A process's first report after the agent starts has nothing to compare with and carries no CPU%; exited processes are let go. procs_test.go pins the arithmetic, the first-report rule and that an idle interval reads near zero where the lifetime average does not (mutation-checked: putting CPUPercent back fails two tests). gofmt, go vet (linux, windows, darwin) and go test -race pass in golang:1.25. Also corrects the Docker Swarm dashboard template's note: container CPU is a percent of one core recorded as "%", not a 0-1 ratio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… project at once Ingest now records docker_stats' and podman_stats' percents as "%", and a project's metric catalog row moves to "%" with its next Docker or Podman batch. RecordContainerPercentMetricUnits corrects every row at once - including projects whose hosts no longer report - so the memory percents already stored (right in value all along) read right in every chart from the moment the update is deployed. It selects container.memory.percent and container.cpu.percent rows that say "1", and container.cpu.utilization rows only with docker_stats' own description: the Kubernetes agent sends that name as CPU cores, where "1" is right. Ids are collected before the first write, each write is a compare-and-set of "%" over "1", one failed write costs no other row its correction, a failed read stops the chain. Registered before MoveHuntressConnectionsToWorkflows, which keeps the slot right before the last for itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t of The docker_stats receiver describes container.memory.percent as "Percentage of memory used", which leaves out the base. The Docker, Podman and Docker Swarm agents now set it to "Memory in use as a percent of the container's memory limit, or of the host's memory when the container has no limit." in the same transform that gives it the unit "%". The Ops suite pins the statement in all three configs, and the behaviour helper checks the description the real collector exports. The App fixture of the fixed agent is captured again from that collector (a quarter core 25%, one and a half cores 150%), and now carries the resource schemaUrl too, so every block it builds equals the captured one field for field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…series
When a project sends container.cpu.utilization from both a container stats
receiver ("%") and Kubernetes (cores, "1"), the stored unit stays "%". The
Kubernetes pages compute their own percent from the cores, but the metrics
explorer then shows a Kubernetes series of that name in cores, unscaled.
The comment on keepsStoredPercentUnit now says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-percent-metrics Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The behaviour script's run of each agent's whole metrics pipeline against the Docker daemon now also requires the CPU and memory percent descriptions the agents set, as its synthetic part already did, and its message for the synthetic memory check says what changes (the unit and the description). Each new check was broken once, with docker_stats' own descriptions put back into a real capture, and failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cription Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… description Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Since a query with its own transformValue gets its values as stored, two
Kubernetes paths drew the wrong number:
- No allocatable CPU to divide by (the cluster sends no
k8s.node.allocatable_cpu, or a node page before its node object loads):
the transform returned the cores as they were, so 0.05 cores drew as
"0.05%". It used to arrive already times 100 from the fetch. It now draws
percent of one core itself (KubernetesCpuUtils.coresToPercentOfOneCore).
- The pages passed the transform only once the allocatable lookup had
loaded. The first fetch then came back converted ("1" to "%", times
100), and MetricView did not fetch again when the transform arrived, so
it ran on those values: the 100x the transform skip was meant to fix.
Every Kubernetes CPU query now carries the transform from its first
fetch (null lookup: percent of one core), and MetricView fetches again
whenever a query gains or loses its own transform.
Tests: ChartPercentScaledOnce (no allocatable while loading or at all,
whatever the catalog unit; node page before its node loads; a source
guard that no Kubernetes CPU query waits for the lookup) and the new
MetricViewTransformRefetch. Each change was broken once and its test
failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metrics list and the explorer's chart header show a metric's description on one truncated line. At 1280 px both new descriptions were cut before the part that says what the number is: "...as a percent of one CPU core (100% is on..." and "...or of the host's memory when the c...". They now fit and lead with it, worded like the docs: - container.cpu.utilization: "CPU used over each collection interval, where 100% is one full CPU core." - container.memory.percent: "Memory in use as a percent of the container's limit, or of host memory if it has none." Same in the Docker, Podman and Swarm agents. The fixed-agent fixture was captured again with the real collector 0.161.0 against Docker 29.8.1 (25.0% and 150.0% again; every block equals the capture field for field), and the container behaviour script passed for all three agents with the new descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heir metric list whole - The old agents' container CPU is a container's lifetime average over the host's uptime: flat, at whatever level that gives. The customer's two hosts read 0.07% and 7.17%. The troubleshooting titles (Docker and Podman agents, "Task CPU" for Swarm) and the Docker and Podman monitor pages' "A CPU alert never fires" now say "a flat line", without "near zero", in all 17 languages (a deletion in each; no anchor moves). - The Docker and Podman agent READMEs had the CPU paragraph in the middle of the "Collected Metrics" list, splitting it in two. The list is whole again, with two short notes after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The customer's report, checked
A customer running
oneuptime/docker-agent:release(agent 12.0.20) sawcontainer.cpu.utilizationas a flat line (0.07% on one host, 7.17% on the other, for the same container using about 1% of a core) andcontainer.memory.percentexactly 100 times too high (traefik: 175 MB of a 4.01 GB limit, 4.36%, shown as 436.64%). Both are real, both are still there on master's agent, and they have different causes.1. CPU: the docker_stats receiver computes it from one reading (agent / upstream)
The Docker, Podman and Docker Swarm agents sent
container.cpu.utilizationas the collector'sdocker_statsreceiver computes it:(Δ container CPU / Δ host CPU) × online CPUs × 100, with the previous reading taken fromprecpu_statsof the same stats response. Since collector-contrib 0.150.0 the receiver's Docker client (moby/moby/clientv0.4+) callsContainerStatswithContainerStatsOptions{}, which sendsstream=false&one-shot=true: the daemon returns a single sample andprecpu_statsis empty. The receiver then divides all the CPU the container used since it started by all the CPU the host used since it booted - a lifetime figure, diluted by host uptime, that barely moves and never follows the load. (Checked in the contrib source for every tag 0.118-0.163 and onmain: 0.149.0 and earlier asked for the previous sample, 0.150.0 and later do not; nothing upstream fixes it yet.) OneUptime's agents ran 0.118.0 until June 2026 and 0.154.0 / 0.161.0 since, so every Docker and Swarm agent from June on has the flat line. Podman hosts always had it: Podman's Docker-compatible API fillsprecpu_statsonly when streaming.Reproduced on this machine (Docker 29.8.1, collector 0.161.0, the shipped receiver block): a busybox container held to a quarter of a core (
--cpus=0.25, busy loop) read 0.0140, then 0.0141, whilecontainer.cpu.usage.totalrose by exactly 25% of a core per second.2. Memory (and CPU) percent: declared "1", read as a 0-1 fraction (OneUptime)
docker_stats(andpodman_stats) sendcontainer.memory.percentandcontainer.cpu.utilizationas a percent (0-100, CPU above 100 on several cores) but declare the unit1, the UCUM unit of a 0-1 fraction. OneUptime keeps one unit per metric name per project (MetricType) and read them as fractions everywhere a unit decides a number: the metrics explorer and dashboards (ValueFormatter: unit1+ a.percent/.utilizationname → ×100), the Docker / Podman host Metrics tab, container detail charts and Swarm Insights (MetricResultUnitConverter:1→%legend is ×100), generic Metrics monitors'%thresholds (MetricUnitUtil), and the container / Swarm task rows ingest mirrors (cpuValueToPercentmultiplied every1by 100). The values themselves were right: 4.3664 stored, 436.64% drawn. The Docker/Podman Overview and Containers tabs, which draw the raw value with a%, and the platform monitors, which use the Docker/Podman/Swarm catalogs (%), were the only places that read them right.Expected behaviour (decided here)
container.cpu.utilization(Docker, Podman, Swarm) = the CPU the container used over each collection interval (30 s), as a percent of one core: 100% is one core busy for the whole interval, 250% two and a half cores. It is the per-second rate ofcontainer.cpu.usage.totaldivided by 10^7, so the two charts always agree and short bursts count in proportion. Percent of one core because that is whatdocker stats/podman statsshow, what OneUptime's Docker/Podman/Swarm catalogs, pages, alert templates ("Above 80 (% of one core)") and docs already promise, and what the customer expects (10 ms/s ≈ 1%). Not % of the host (a container pinning one core on a 32-core host would read 3%) and not % of its limit (most containers have none). An interval average rather thandocker stats' one-second sample, because a one-second sample every 30 s misses most spikes - exactly what the customer saw.container.memory.percent= memory in use (inactive file cache excluded) as a percent of the container's memory limit, or of the host's memory when it has none, 0-100, unit%, shown once as a percent.What changed
Agents (
agents/DockerAgent,agents/PodmanAgent,agents/DockerSwarmAgent,otel-collector-config.yaml):container.cpu.utilizationis off. Four processors build it fromcontainer.cpu.usage.total:transform/cpu-utilization-source(copy),cumulative_to_delta/cpu-utilization(CPU time since the previous scrape;initial_value: drop,max_staleness: 10m; a counter reset after a container restart drops one point instead of sending a negative one),delta_to_rate/cpu-utilization(per second),transform/percent-units(scale_metric(1e-7, "%"), a description that says what it is). The same last processor givescontainer.memory.percentthe unit%and a description that says what it is a percent of (the container's memory limit, or the host's memory without one; docker_stats only says "Percentage of memory used").container.cpu.usage.totalgoes on unchanged. Identical in all three agents; only the metrics pipeline runs them.docker restart; memory percent equal to usage / limit × 100.Ingest (
OtelMetricsIngestService, newCommon/Utils/Metrics/ContainerStatsPercentUnit):docker_statsorpodman_statspercent declared1- told apart by its instrumentation scope (.../receiver/dockerstatsreceiver,.../receiver/podmanreceiver, and the olderotelcol/...names) - is recorded as%in the metric catalog and read as%by the container snapshots. This fixes agents already in the field and anyone running these receivers in their own collector, without an agent upgrade. The Kubernetes agent'scontainer.cpu.utilization(CPU cores, declared1, scopekubeletstatsreceiver) is left alone.cpuValueToPercentno longer multiplies docker_stats' percent: container rows (Docker, Podman, Swarm tasks, cloud instances from docker_stats sidecars) hold 7.17, not 717.container.cpu.utilizationfrom both (cores1/ percent%). The catalog keeps the%a container stats receiver set (TelemetryUtilreconcile,keepsStoredPercentUnit) instead of flipping - and writing - on every other batch.RecordContainerPercentMetricUnits: every project's storedcontainer.memory.percent/container.cpu.percentrows that say1, andcontainer.cpu.utilizationrows with docker_stats' own description, become%at once (compare-and-set; Kubernetes rows untouched), so stored memory percents read right from the moment this deploys, including projects whose hosts no longer report.The same flaw elsewhere, fixed:
1with legend%and atransformValuethat divides by the node's allocatable cores ×100 - after the fetch had already converted1→%(×100). A pod using 1.25% of a 4-core node was drawn at 125%. A query with its owntransformValuenow gets its values as stored (MetricResultUnitConverter); queries without one convert exactly as before. Every Kubernetes CPU query carries its transform from the first fetch (percent of one core until the node's allocatable CPU is known), and MetricView fetches again when a query gains or loses a transform (UI review, below). Mutation-checked: removing the skip fails the Kubernetes and Proxmox cases.agents/InfrastructureAgentreported each process'scpuPercentwith gopsutil'sProcess.CPUPercent, which is CPU time since the process started / its age - the same flat line. It now keeps each process's CPU time from the previous 30-second report (keyed by PID + start time, so a reused PID is not compared with another process) and reports the CPU used between the two as a percent of one core (astop). First report after start: no CPU%. Mutation-checked.Display texts and docs:
container.cpu.percent(docker_stats never sends it) and now say howcontainer.cpu.utilizationis computed.What changes for existing customers
docker pull oneuptime/docker-agent:releaseand recreate). From the second reading after the upgrade, CPU follows the load. Swarm: the agent's version is its collector's, which did not change, so no warning sign appears; re-run the installer or download the agent's files again (documented).container.cpu.utilizationis one metric name with two quantities (Kubernetes cores, Docker percent); its unit settles on%, so Kubernetes series of that name read as raw cores there. The Kubernetes pages compute their own percent and are not affected.%either way); it settles once every agent is upgraded.Left out, deliberately
IncludePreviousSample: trueorstream_stats); I did not open an upstream issue. If upstream fixes it, the agents can keep these processors (they are the interval average, which is what we want either way).monitor/server-monitor.mdsays the process list carries "CPU %" - still true; not reworded (would need 16 translations for no change of meaning).Tests (what each proves)
Tests/Ops/ContainerAgentCpuUtilization.test.js(24): the three configs turn the receiver's CPU percent off and keep the counter, define the four processors exactly and identically (units and descriptions included), run them in order aftermemory_limiterin the metrics pipeline only, under the 0.161.0 names (not the deprecated aliases), staleness ≥ 10 scrapes; the arithmetic (10^9 ns/s = 100%; 25 / 150 / 0 / restart / 10 / 30%; 1.2% for 12 ms/s at any interval).Tests/Ops/container-agent-cpu-utilization-behaviour.sh(new CI step in Ops Config Test, ~1 min): each agent's processors in the real collector image, fed six scrapes of two containers (25, 150, 0, nothing after a restart, 10; 1% throughout), the counter unchanged and the memory percent the same values in%; then each agent's whole metrics pipeline against the runner's Docker daemon with a container held to half a core: 50% ±15, equal to the rate ofcontainer.cpu.usage.total, memory percent = usage / limit × 100 in%; both percents carry the new descriptions in both parts. Its checks were each broken once (old descriptions in a real capture) and failed.packages/App/Tests/Telemetry/OtelMetricsIngestContainerPercent.test.ts(19) with a new real capture of the fixed agent (Fixtures/DockerAgentCpuCapture.ts, two consecutive scrapes, 25% and 150%; every block it builds equals the captured one field for field) and the existing capture of the old one: catalog units%for both percents (old and new agent), the customer's 4.3664 stored as sent under%, container rows not ×100 (Docker, Podman, Swarm task), the captured CPU percent equals the rate of the captured counter, Kubernetes' cores stay1, other producers/units untouched.packages/Common/Tests/Utils/Metrics/ContainerStatsPercentUnit.test.ts(38): the scope/name/unit table, the customer's 436.64% vs 4.37% throughValueFormatter,%thresholds no longer ×100, Kubernetes cores still a percent of one core, the sticky rule's truth table.packages/Common/Tests/Server/Utils/MetricTypeContainerPercentUnit.test.ts(10): a stored1becomes%with the first batch; interleaved Kubernetes/Docker batches settle on%with exactly one write; Kubernetes-only projects keep1; other metrics' unit changes unchanged.packages/Common/Tests/App/Dashboard/ChartPercentScaledOnce.test.ts(12): the fetch's conversion then the chart's transform, as the pages run them: Kubernetes container/pod/node CPU 1.25% / 75% (not 125% / 7500%) whatever the catalog unit, Proxmox ratio once, docker_stats percents as themselves, and queries without a transform still converting.packages/App/Tests/Workers/DataMigrations/RecordContainerPercentMetricUnits.test.ts(8): registration and slot, the exact row selection (docker_stats' description for CPU), ids collected across pages before writing, compare-and-set%over1, a failed write costs no other row, a failed read stops the chain, idempotent, rollback no-op.agents/InfrastructureAgent/utils/procs_test.go(4): the per-process arithmetic and its no-previous cases, CPU% only from the second report, an idle interval reads near zero where gopsutil's lifetime average reads 34%, exited processes forgotten.OtelMetricsIngestContainerSnapshot(rows no longer ×100),OtelMetricsCloudSnapshotcomments,DockerSwarmMetricTooltips(the scale test now reads the fixed ingest and requires "100% is one full CPU core").Mutation checks (one break at a time, each caught): ingest scope normalization removed (7 tests fail), sticky rule removed (2), converter transform-skip removed (6), host agent back to
CPUPercent(2).Ran locally
otelcol validateon all three configs; the new behaviour script end to end after the last change (all three agents, synthetic + real daemon, 56 checks).Guard(149 suites, 4,730 tests) andLocale(30 suites); the App suites that read them (99 suites), App root-level tests (17) andLocale(9). After the final merge (master at a104399, which brought only tests, the NormalizeListOrder fix and the Dashboard tsc heap): every App DataMigrations suite (44 suites, 718 tests), the docs suites for the six pages (9 suites, 6,348 tests) and the general docs suites (29 suites, 1,419 tests),docs:check-anchors,i18n:validate, the Ops jest suite (62 suites, 3,594 tests), and the App ingest suites with the re-captured fixture (3 suites, 70 tests).npm run compile(Common, App),compile-tests(Common), Dashboardtsc, Dashboardi18n:check,DashboardLocalesGuardand the Common suites for the changed modules (4 suites, 143 tests). gofmt,go vet(linux, windows, darwin) andgo test ./... -racefor the host agent ingolang:1.25. prettier and eslint on every changed code file: clean, except two lines of the PodmanAgent config that come from master (prettier would requote a regex and rewrap a list; master's copy fails the same way, left as they are) and the Ops.jssuites, which eslint's TypeScript project does not include (as for every Ops suite).Seen failing on master while this was built, not caused here:
Tests/Workers/DataMigrations/NormalizeListOrder("covers exactly the lists that are put in order by dragging":AIPostmortemRulewas not covered since #5071). Master fixed it in 6d2cec5; it passes after the merge.Reply to the customer (draft)
UI review
Looked at. I rendered the real components in an esbuild harness with the production Tailwind build and Inter, and mocked the network so it answers like the real endpoints (
AnalyticsModelAPI.aggregate, the metric catalog,ModelAPI). Shots were taken in headless Chromium at 1280 px and 390 px, light and dark, in English and German:1, old agent) reads memory 436.64% and CPU a flat 7.17%, as in the report. "After" reads 4.37%, and CPU follows the load (bursts to 6.2%).1or%. With no allocatable reading it draws percent of one core.Fixed.
Kubernetes CPU charts: two regressions of the transform skip (functional, found in review):
k8s.node.allocatable_cpu, or a node page before its node object loads): the transform returned cores unchanged, which used to arrive already ×100 from the fetch. 0.05 cores drew as "0.05%", 100 times too low. It now draws percent of one core (5%), as before this PR (KubernetesCpuUtils.coresToPercentOfOneCore).1to%, ×100), and MetricView did not fetch again when the transform arrived, so the transform ran on converted values: 125% again. Every Kubernetes CPU query now carries its transform from the first fetch. MetricView also fetches again whenever a query gains or loses its own transform, which keeps the converter's "values as stored" contract for any page.Metric descriptions cut off. Both new descriptions were cut at 1280 px, in the metrics list and in the explorer's chart header, before the part that says what the number is ("...one CPU core (100% is on..."). They now fit on one line and are worded like the docs:
container.cpu.utilization: "CPU used over each collection interval, where 100% is one full CPU core."container.memory.percent: "Memory in use as a percent of the container's limit, or of host memory if it has none."Same in all three agents. I captured the fixed-agent fixture again with the real collector 0.161.0 against Docker 29.8.1 (25.0% and 150.0% again; every block equals the capture field for field). The behaviour script passed for all three agents with the new descriptions.
Docs: "a flat line near zero" is now "a flat line", in the three troubleshooting titles and the monitor pages' sentence, in all 17 languages. The old figure is a container's lifetime average over the host's uptime, so it can sit at any level: the customer's staging host read a flat 7.17%. Each edit deletes the phrase; no anchor moves.
READMEs: the CPU paragraph sat in the middle of the "Collected Metrics" list and split it in two. The list is whole again, with two short notes after it.
Checked and left alone. The Swarm CPU tooltips read well in English and the 16 translations, at both widths and in both themes. The Persian docs render right to left correctly.
Follow-ups I saw that this change did not cause:
referenceLines,formattedExemplarPoints,onExemplarClick,anomalyBand*) to adiv, which only shows up as React dev warnings.Tests the review added or changed:
packages/Common/Tests/App/Dashboard/MetricViewTransformRefetch.test.tsx(new, 3 tests). A transform that arrives after the first fetch is fetched with. A transform made anew on every render does not refetch. A transform that goes away is fetched without.packages/Common/Tests/App/Dashboard/ChartPercentScaledOnce.test.ts(+5 tests):coresToPercentOfOneCore;Tests/Ops/ContainerAgentCpuUtilization.test.js,Tests/Ops/Utils/ContainerAgentCpuBehaviour.jsandpackages/App/Tests/Fixtures/DockerAgentCpuCapture.ts: the new descriptions, and the re-captured values.Ran after the review's last change (master had not moved since a104399):
Guard(149 suites, 4,730 tests).docs:check-anchorsandi18n:validate.ee/hidden.compileandcompile-tests, Appcompile, Dashboardtsc, and--listFilesfor Common and App (no source outside the package and Common)..jssuites, which eslint's TypeScript project does not include.🤖 Generated with Claude Code