[receiver/hostmetrics] Add cputicks reader behind feature gate#47972
[receiver/hostmetrics] Add cputicks reader behind feature gate#47972salvatore-campagna wants to merge 28 commits into
Conversation
Add a Linux-only cputicks package that reads raw uint64 tick counts directly from /proc/stat, bypassing gopsutil's float64 conversion that loses integer precision. Replace the scraper's direct times+ucal fields with a pluggable emitCPUMetrics emitter, and wire the new cputicks path behind the receiver.hostmetricsreceiver.UseCPUTicks alpha feature gate. When enabled, CPU time and utilization metrics use precision.Scale and precision.Ratio to preserve the true precision of the source data. Non-Linux platforms continue using gopsutil unchanged. Ref open-telemetry#46177
533ca96 to
8fc2d67
Compare
|
I think the |
| // Config relating to CPU Metric Scraper. | ||
| type Config struct { | ||
| metadata.MetricsBuilderConfig `mapstructure:",squash"` | ||
| rootPath string |
There was a problem hiding this comment.
I don't have a strong opinion as it seems the filesystem scraper has a similar approach to use the rootPath, but an alternative would be extracting it from the context: https://github.com/salvatore-campagna/opentelemetry-collector-contrib/blob/fix/hostmetrics-cputicks-package/receiver/hostmetricsreceiver/internal/scraper/processscraper/process.go#L132
| findMetric := func(name string) pmetric.Metric { | ||
| for _, m := range metrics.All() { | ||
| if m.Name() == name { | ||
| return m | ||
| } | ||
| } | ||
| t.Fatalf("metric not found: %s", name) | ||
| return pmetric.Metric{} | ||
| } | ||
|
|
||
| findDP := func(metric pmetric.Metric, cpuName, state string) float64 { |
There was a problem hiding this comment.
Are you familiar with the pkg/golden and pmetrictest packages? I think we can use it here to avoid this lookup functions (already implemented in pmetrictest)
And this helper function to write the metrics on the first run: golden.WriteMetrics(t, expectedFile, md)
There was a problem hiding this comment.
Thanks for pointing me to golden and pmetrictest, I wasn't aware of them.
The Linux kernel already accounts Guest ticks inside User and GuestNice inside Nice. Including them in Total() double-counts them, inflating the denominator and deflating utilization ratios. Ref open-telemetry#48024
# Conflicts: # receiver/hostmetricsreceiver/go.mod
rogercoll
left a comment
There was a problem hiding this comment.
LGTM! It would be great having a storage analysis of this improvement before moving the feature gate to beta 🎉
# Conflicts: # receiver/hostmetricsreceiver/go.mod
|
This PR was marked stale due to lack of activity. It will be closed in 14 days. |
|
We touched on this in the System Semantic Conventions SIG yesterday. I have to push back on this. I should have flagged concerns on #46153 before approving it, and this PR builds further on the same approach while adding much more complexity. I don't think this complexity can be justified. From my point of view, rounding a float's mantissa is meaningless by design as it's encoded as binary fractions, not as decimals. The PRs call the additional digits "arithmetic noise", but we cannot remove that noise we just push it further away behind more zeroes, e.g. 3.3333333333...4 becomes something like 3.330000000000...03232434 If users want values rounded this way, the transform processor is a better place, where it can be implement an OTTL function. Receivers should always optimize for accuracy of the emitted values not for their representation. |
Thanks for raising this and for the SIG discussion context. I think the numbers help separate the two phenomena here. IEEE 754 representation (unavoidable). Absolutely right that some decimal values cannot be represented exactly in binary floating point. No code can fix that. Early conversion of source counters in the processing pipeline (avoidable). The Linux kernel exposes CPU tick counts as exact integers in This PR reads The source data in this case is a set of integer tick counters. This change keeps those counters as integers through the delta computation and only converts to float64 when the utilization ratio is computed. Reproducible evidence. The following is generated by a Go program that simulates both paths with the same tick delta (333/3333) at five consecutive absolute positions: Two observations from this data:
The important point here is not the final decimal representation. The important point is that the two computation paths produce different results because one computes the delta from scaled float64 values and the other computes the delta from the original integer counters. Regarding downstream processing. A transform processor operates on the emitted utilization value. It can round or otherwise transform that value, but it does not have access to the original integer counters or integer deltas. Once the receiver emits: the information that the ratio originated from: is no longer available downstream. For that reason, computing utilization from integer deltas versus scaled float64 values is a receiver-side concern. A downstream processor can change the representation of the emitted value, but it cannot reconstruct the original integer-delta computation after the fact.
Problem vs. solution. The data above shows a measurable difference between the current computation path and one that preserves integer counters through the delta computation. The approach was discussed in #46177 and this PR implements it behind an alpha feature gate. If there are concerns about code structure, package layout, or maintenance cost, I am happy to discuss those. Before discussing implementation details, do we agree on the following observations?
If any of those observations are not supported by the data, I am happy to revisit the analysis. |
|
For context on why this is not handled upstream: the only gopsutil fix that would work is a new API exposing raw integer ticks, with per-platform implementations across all supported OSes. That was discussed in #46177. gopsutil does not expose the raw integers and there is no indication that it will. The cputicks reader exists because of that constraint. |
|
During the System SIG on 04/06/2026 we discussed whether https://github.com/open-telemetry/opentelemetrycollector-contrib/pull/46153 could be moved into a processor or OTTL function. The core challenge is that the PR does not apply fixed-decimal truncation, it derives the correct number of significant digits from the magnitude of the original integer inputs, so the rounding is context-aware and cannot be reconstructed once the float64 is emitted. Two cases:
|
|
This PR was marked stale due to lack of activity. It will be closed in 14 days. |
|
Not stale. During System WG SIG on 04/06/2026 it has discussed if there was any gopsutil feature to retrieve the raw cputicks using the package. It seems that now at the moment, but it could be extended using the |
Resolve conflicts in: - receiver/hostmetricsreceiver/go.mod: bump to v0.154.0 / collector v1.60.1, keep tklauser/go-sysconf and pkg/golden direct deps - receiver/hostmetricsreceiver/metadata.yaml: keep UseCPUTicks feature gate alongside upstream skip_strict_validation addition
Add skip_strict_validation: true to the receiver.hostmetricsreceiver.UseCPUTicks feature gate to match how the other two pre-existing gates in this file (hostmetrics.process.bootTimeCache and receiver.hostmetricsreceiver.UseLinuxMemAvailable) bypass the new mdatagen prefix check. The required prefix receiver.host_metrics. cannot be used as the ID because the underscore character is not allowed in feature gate IDs.
…change PR open-telemetry#49161 made the cpu attribute opt-in for system.cpu.time and system.cpu.utilization and enabled system.cpu.logical.count by default. The cputicks tests need to explicitly opt back in to per-CPU attributes (since that is the core behavior being tested) and disable the logical count metric to keep metric count assertions correct.
Apply gci grouping (stdlib before third-party) to generated_package_test.go and generated_logs_test.go, which mdatagen produced with mixed import groups. Required for the porto-and-gci CI shard.
Summary
Follow-up to #46153 (precision improvements for memory, disk, and filesystem scrapers) and the approach discussed in #46177. The CPU scraper was excluded from #46153 because gopsutil converts raw integer jiffies from
/proc/stattofloat64seconds before the scraper sees them. This shrinks operand magnitudes so thatprecision.Ratiorounds too aggressively; a 1-CPU/10s-interval machine gets only 1 significant digit, rounding 3% utilization to 0%.As discussed in #46177, a gopsutil upstream fix wouldn't help here since the consumer still computes
delta_user / delta_totalasfloat64 / float64, producing 15-17 digits again. The fix needs to keep the data asuint64throughout the pipeline. The new path is behind an alpha feature gate for opt-in testing before becoming the default.This PR adds a Linux-only
cputickspackage and integrates it into the CPU scraper, combining the infrastructure and wiring proposed in #46177 into a single PR. The package is scoped tocpuscraper/internal/cputicks/since it is only used by the CPU scraper. Linux-only is pragmatic: the vast majority of OTel collectors in production run on Linux, and macOS/Windows native readers can follow in separate PRs if needed.The
cputicks.Readerparses/proc/statdirectly usingbufio.Scanner+strconv.ParseUint, returning[]Statwith rawuint64fields. It handles variable column counts across kernel versions and supportsrootPathfor container environments where the host procfs is mounted at a non-default location.To wire the new reader in without breaking existing behavior, the scraper's direct
times+ucalfields are replaced with a singleemitCPUMetricsfunction field that encapsulates the full read+record cycle.newGopsutilEmitterwraps the existing gopsutil+ucal logic unchanged;newCputicksEmitterusescputicks.Readerwithprecision.Scalefor tick-to-seconds conversion andprecision.Ratiofor utilization, preserving the true precision of the source data instead of producing 15-17 digit float64 artifacts.The
receiver.hostmetricsreceiver.UseCPUTicksalpha feature gate selects between the two emitters at construction time. When disabled (default) or on non-Linux platforms, behavior is unchanged andnewGopsutilEmitteris used. Non-Linux platforms continue using gopsutil as noted in #46177 since reconstructing integer ticks from gopsutil'sfloat64output would be a lossy round-trip.Stat.Total()excludes Guest and GuestNice because the Linux kernel already accounts them inside User and Nice respectively (#48024).Testing
Benchmarks
Reading
/proc/statdirectly does not introduce a performance penalty compared to gopsutil:Ref #46177