Skip to content

Commit 533ca96

Browse files
[receiver/hostmetrics] Add cputicks reader behind feature gate
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 metrics use precision.Scale for tick-to-seconds conversion and utilization ratios use precision.Ratio, preserving the true precision of the source data (3-4 significant digits) instead of producing 15-17 digit float64 artifacts. Non-Linux platforms continue using gopsutil unchanged. Closes #46177
1 parent d425935 commit 533ca96

12 files changed

Lines changed: 816 additions & 59 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: 'enhancement'
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: receiver/hostmetrics
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add cputicks package and integrate with CPU scraper behind feature gate
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [46177]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: |
19+
New internal package that reads raw uint64 CPU ticks directly from /proc/stat
20+
on Linux, avoiding the precision loss from gopsutil's float64 seconds conversion.
21+
Enabled via the receiver.hostmetricsreceiver.UseCPUTicks feature gate (alpha).
22+
23+
# If your change doesn't affect end users or the exported elements of any package,
24+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
25+
# Optional: The change log or logs in which this entry should be included.
26+
# e.g. '[user]' or '[user, api]'
27+
# Include 'user' if the change is relevant to end users.
28+
# Include 'api' if there is a change to a library API.
29+
# Default: '[user]'
30+
change_logs: [user]

receiver/hostmetricsreceiver/internal/scraper/cpuscraper/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@ import (
1010
// Config relating to CPU Metric Scraper.
1111
type Config struct {
1212
metadata.MetricsBuilderConfig `mapstructure:",squash"`
13+
rootPath string
14+
}
15+
16+
func (cfg *Config) SetRootPath(rootPath string) {
17+
cfg.rootPath = rootPath
1318
}

receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ type cpuScraper struct {
3030
settings scraper.Settings
3131
config *Config
3232
mb *metadata.MetricsBuilder
33-
ucal *ucal.CPUUtilizationCalculator
3433

3534
// for mocking
36-
bootTime func(context.Context) (uint64, error)
37-
times func(context.Context, bool) ([]cpu.TimesStat, error)
38-
now func() time.Time
35+
bootTime func(context.Context) (uint64, error)
36+
emitCPUMetrics func(context.Context, pcommon.Timestamp, *metadata.MetricsBuilder) error
37+
now func() time.Time
3938
}
4039

4140
type cpuInfo struct {
@@ -45,7 +44,29 @@ type cpuInfo struct {
4544

4645
// newCPUScraper creates a set of CPU related metrics
4746
func newCPUScraper(_ context.Context, settings scraper.Settings, cfg *Config) *cpuScraper {
48-
return &cpuScraper{settings: settings, config: cfg, bootTime: host.BootTimeWithContext, times: cpu.TimesWithContext, ucal: &ucal.CPUUtilizationCalculator{}, now: time.Now}
47+
return &cpuScraper{
48+
settings: settings,
49+
config: cfg,
50+
bootTime: host.BootTimeWithContext,
51+
emitCPUMetrics: newCPUEmitter(cfg),
52+
now: time.Now,
53+
}
54+
}
55+
56+
func newGopsutilEmitter(times func(context.Context, bool) ([]cpu.TimesStat, error)) func(context.Context, pcommon.Timestamp, *metadata.MetricsBuilder) error {
57+
calc := &ucal.CPUUtilizationCalculator{}
58+
return func(ctx context.Context, now pcommon.Timestamp, mb *metadata.MetricsBuilder) error {
59+
cpuTimes, err := times(ctx, true)
60+
if err != nil {
61+
return err
62+
}
63+
for _, cpuTime := range cpuTimes {
64+
recordCPUTimeStateDataPoints(now, cpuTime, mb)
65+
}
66+
return calc.CalculateAndRecord(now, cpuTimes, func(now pcommon.Timestamp, u ucal.CPUUtilization) {
67+
recordCPUUtilization(now, u, mb)
68+
})
69+
}
4970
}
5071

5172
func (s *cpuScraper) start(ctx context.Context, _ component.Host) error {
@@ -59,17 +80,8 @@ func (s *cpuScraper) start(ctx context.Context, _ component.Host) error {
5980

6081
func (s *cpuScraper) scrape(ctx context.Context) (pmetric.Metrics, error) {
6182
now := pcommon.NewTimestampFromTime(s.now())
62-
cpuTimes, err := s.times(ctx, true /*percpu=*/)
63-
if err != nil {
64-
return pmetric.NewMetrics(), scrapererror.NewPartialScrapeError(err, metricsLen)
65-
}
6683

67-
for _, cpuTime := range cpuTimes {
68-
s.recordCPUTimeStateDataPoints(now, cpuTime)
69-
}
70-
71-
err = s.ucal.CalculateAndRecord(now, cpuTimes, s.recordCPUUtilization)
72-
if err != nil {
84+
if err := s.emitCPUMetrics(ctx, now, s.mb); err != nil {
7385
return pmetric.NewMetrics(), scrapererror.NewPartialScrapeError(err, metricsLen)
7486
}
7587

receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_linux.go

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,151 @@
66
package cpuscraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper"
77

88
import (
9+
"context"
10+
"fmt"
11+
"time"
12+
913
"github.com/prometheus/procfs"
1014
"github.com/shirou/gopsutil/v4/cpu"
15+
"github.com/tklauser/go-sysconf"
16+
"go.opentelemetry.io/collector/featuregate"
1117
"go.opentelemetry.io/collector/pdata/pcommon"
1218
"go.opentelemetry.io/collector/scraper/scrapererror"
1319

20+
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/precision"
21+
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/cputicks"
1422
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata"
1523
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/ucal"
1624
)
1725

18-
func (s *cpuScraper) recordCPUTimeStateDataPoints(now pcommon.Timestamp, cpuTime cpu.TimesStat) {
19-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.User, cpuTime.CPU, metadata.AttributeStateUser)
20-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.System, cpuTime.CPU, metadata.AttributeStateSystem)
21-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Idle, cpuTime.CPU, metadata.AttributeStateIdle)
22-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Irq, cpuTime.CPU, metadata.AttributeStateInterrupt)
23-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Nice, cpuTime.CPU, metadata.AttributeStateNice)
24-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Softirq, cpuTime.CPU, metadata.AttributeStateSoftirq)
25-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Steal, cpuTime.CPU, metadata.AttributeStateSteal)
26-
s.mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Iowait, cpuTime.CPU, metadata.AttributeStateWait)
27-
}
28-
29-
func (s *cpuScraper) recordCPUUtilization(now pcommon.Timestamp, cpuUtilization ucal.CPUUtilization) {
30-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.User, cpuUtilization.CPU, metadata.AttributeStateUser)
31-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.System, cpuUtilization.CPU, metadata.AttributeStateSystem)
32-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Idle, cpuUtilization.CPU, metadata.AttributeStateIdle)
33-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Irq, cpuUtilization.CPU, metadata.AttributeStateInterrupt)
34-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Nice, cpuUtilization.CPU, metadata.AttributeStateNice)
35-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Softirq, cpuUtilization.CPU, metadata.AttributeStateSoftirq)
36-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Steal, cpuUtilization.CPU, metadata.AttributeStateSteal)
37-
s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Iowait, cpuUtilization.CPU, metadata.AttributeStateWait)
26+
var useCPUTicks = featuregate.GlobalRegistry().MustRegister(
27+
"receiver.hostmetricsreceiver.UseCPUTicks",
28+
featuregate.StageAlpha,
29+
featuregate.WithRegisterDescription("Use raw uint64 CPU tick counts from /proc/stat instead of gopsutil for improved precision in CPU time and utilization metrics."),
30+
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/46177"),
31+
featuregate.WithRegisterFromVersion("v0.152.0"),
32+
)
33+
34+
const defaultTicksPerSecond = 100
35+
36+
// tickReader reads per-CPU tick counts from the operating system.
37+
// Defined at the consumer for testability.
38+
type tickReader interface {
39+
ReadAll(ctx context.Context) ([]cputicks.Stat, error)
40+
TicksPerSecond() uint64
41+
}
42+
43+
func clockTicksPerSecond() uint64 {
44+
clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK)
45+
if err != nil || clkTck <= 0 {
46+
return defaultTicksPerSecond
47+
}
48+
return uint64(clkTck)
49+
}
50+
51+
func newCPUEmitter(cfg *Config) func(context.Context, pcommon.Timestamp, *metadata.MetricsBuilder) error {
52+
if useCPUTicks.IsEnabled() {
53+
return newCputicksEmitter(cputicks.NewReader(cfg.rootPath, clockTicksPerSecond()))
54+
}
55+
return newGopsutilEmitter(cpu.TimesWithContext)
56+
}
57+
58+
func newCputicksEmitter(reader tickReader) func(context.Context, pcommon.Timestamp, *metadata.MetricsBuilder) error {
59+
tickDuration := time.Second / time.Duration(reader.TicksPerSecond())
60+
var prevTicks map[string]cputicks.Stat
61+
return func(ctx context.Context, now pcommon.Timestamp, mb *metadata.MetricsBuilder) error {
62+
ticks, err := reader.ReadAll(ctx)
63+
if err != nil {
64+
return err
65+
}
66+
67+
recordTickTimes(now, ticks, tickDuration, mb)
68+
69+
if prevTicks != nil {
70+
currTicks := make(map[string]cputicks.Stat, len(ticks))
71+
for _, t := range ticks {
72+
currTicks[t.CPU] = t
73+
}
74+
for _, prev := range prevTicks {
75+
curr, ok := currTicks[prev.CPU]
76+
if !ok {
77+
return fmt.Errorf("getting ticks for cpu %s: %w", prev.CPU, ucal.ErrTimeStatNotFound)
78+
}
79+
recordTickUtilization(now, prev, curr, mb)
80+
}
81+
}
82+
prevTicks = make(map[string]cputicks.Stat, len(ticks))
83+
for _, t := range ticks {
84+
prevTicks[t.CPU] = t
85+
}
86+
return nil
87+
}
88+
}
89+
90+
func recordTickTimes(now pcommon.Timestamp, ticks []cputicks.Stat, tickDuration time.Duration, mb *metadata.MetricsBuilder) {
91+
for _, t := range ticks {
92+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.User, tickDuration), t.CPU, metadata.AttributeStateUser)
93+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.System, tickDuration), t.CPU, metadata.AttributeStateSystem)
94+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Idle, tickDuration), t.CPU, metadata.AttributeStateIdle)
95+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Irq, tickDuration), t.CPU, metadata.AttributeStateInterrupt)
96+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Nice, tickDuration), t.CPU, metadata.AttributeStateNice)
97+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Softirq, tickDuration), t.CPU, metadata.AttributeStateSoftirq)
98+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Steal, tickDuration), t.CPU, metadata.AttributeStateSteal)
99+
mb.RecordSystemCPUTimeDataPoint(now, precision.Scale(t.Iowait, tickDuration), t.CPU, metadata.AttributeStateWait)
100+
}
101+
}
102+
103+
func recordTickUtilization(now pcommon.Timestamp, prev, curr cputicks.Stat, mb *metadata.MetricsBuilder) {
104+
deltaTotal := curr.Total() - prev.Total()
105+
if deltaTotal == 0 {
106+
recordZeroUtilization(now, curr.CPU, mb)
107+
return
108+
}
109+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.User-prev.User, deltaTotal), curr.CPU, metadata.AttributeStateUser)
110+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.System-prev.System, deltaTotal), curr.CPU, metadata.AttributeStateSystem)
111+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Idle-prev.Idle, deltaTotal), curr.CPU, metadata.AttributeStateIdle)
112+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Irq-prev.Irq, deltaTotal), curr.CPU, metadata.AttributeStateInterrupt)
113+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Nice-prev.Nice, deltaTotal), curr.CPU, metadata.AttributeStateNice)
114+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Softirq-prev.Softirq, deltaTotal), curr.CPU, metadata.AttributeStateSoftirq)
115+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Steal-prev.Steal, deltaTotal), curr.CPU, metadata.AttributeStateSteal)
116+
mb.RecordSystemCPUUtilizationDataPoint(now, precision.Ratio(curr.Iowait-prev.Iowait, deltaTotal), curr.CPU, metadata.AttributeStateWait)
117+
}
118+
119+
func recordZeroUtilization(now pcommon.Timestamp, cpuName string, mb *metadata.MetricsBuilder) {
120+
for _, state := range []metadata.AttributeState{
121+
metadata.AttributeStateUser,
122+
metadata.AttributeStateSystem,
123+
metadata.AttributeStateIdle,
124+
metadata.AttributeStateInterrupt,
125+
metadata.AttributeStateNice,
126+
metadata.AttributeStateSoftirq,
127+
metadata.AttributeStateSteal,
128+
metadata.AttributeStateWait,
129+
} {
130+
mb.RecordSystemCPUUtilizationDataPoint(now, 0, cpuName, state)
131+
}
132+
}
133+
134+
func recordCPUTimeStateDataPoints(now pcommon.Timestamp, cpuTime cpu.TimesStat, mb *metadata.MetricsBuilder) {
135+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.User, cpuTime.CPU, metadata.AttributeStateUser)
136+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.System, cpuTime.CPU, metadata.AttributeStateSystem)
137+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Idle, cpuTime.CPU, metadata.AttributeStateIdle)
138+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Irq, cpuTime.CPU, metadata.AttributeStateInterrupt)
139+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Nice, cpuTime.CPU, metadata.AttributeStateNice)
140+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Softirq, cpuTime.CPU, metadata.AttributeStateSoftirq)
141+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Steal, cpuTime.CPU, metadata.AttributeStateSteal)
142+
mb.RecordSystemCPUTimeDataPoint(now, cpuTime.Iowait, cpuTime.CPU, metadata.AttributeStateWait)
143+
}
144+
145+
func recordCPUUtilization(now pcommon.Timestamp, cpuUtilization ucal.CPUUtilization, mb *metadata.MetricsBuilder) {
146+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.User, cpuUtilization.CPU, metadata.AttributeStateUser)
147+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.System, cpuUtilization.CPU, metadata.AttributeStateSystem)
148+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Idle, cpuUtilization.CPU, metadata.AttributeStateIdle)
149+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Irq, cpuUtilization.CPU, metadata.AttributeStateInterrupt)
150+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Nice, cpuUtilization.CPU, metadata.AttributeStateNice)
151+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Softirq, cpuUtilization.CPU, metadata.AttributeStateSoftirq)
152+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Steal, cpuUtilization.CPU, metadata.AttributeStateSteal)
153+
mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Iowait, cpuUtilization.CPU, metadata.AttributeStateWait)
38154
}
39155

40156
func (*cpuScraper) getCPUInfo() ([]cpuInfo, error) {

0 commit comments

Comments
 (0)