From 8557b2ab69042a5bc6795c5e1e13a47fbfe76216 Mon Sep 17 00:00:00 2001 From: Chris Mark Date: Tue, 31 Oct 2023 21:12:40 +0000 Subject: [PATCH] [receiver/hostmetricsreceiver] Add support for cpu frequency metric (#27445) **Description:** : Added support for host's cpu frequency as part of the hostmetricsreceiver. **Link to tracking Issue:** open-telemetry#26532 **Testing:** 1. Using the following configuration: ```yml receivers: hostmetrics: collection_interval: 5s scrapers: cpu: metrics: system.cpu.frequency: enabled: true processors: resourcedetection/system: detectors: ["system"] system: hostname_sources: ["lookup", "cname", "dns", "os"] resource_attributes: host.name: enabled: true host.id: enabled: true host.cpu.cache.l2.size: enabled: true host.cpu.family: enabled: true host.cpu.model.id: enabled: true host.cpu.model.name: enabled: true host.cpu.stepping: enabled: true host.cpu.vendor.id: enabled: true service: pipelines: metrics: receivers: [hostmetrics] exporters: [file] processors: [resourcedetection/system] exporters: file: path: ./output.json ``` 2. Start the collector with ./bin/otelcontribcol_linux_amd64 --config examples/host_config.yaml 3. The output reports the added metric successfully: ```json { "resourceMetrics":[ { "scopeMetrics":[ { "scope":{ "name":"otelcol/hostmetricsreceiver/cpu", "version":"0.85.0-dev" }, "metrics":[ { "name":"system.cpu.frequency", "description":"Current frequency of the CPU core in MHz.", "unit":"MHz", "gauge":{ "dataPoints":[ { "attributes":[ { "key":"cpu", "value":{ "stringValue":"cpu0" } } ], "startTimeUnixNano":"1696487580000000000", "timeUnixNano":"1696512423758783158", "asDouble":3000 }, { "attributes":[ { "key":"cpu", "value":{ "stringValue":"cpu1" } } ], "startTimeUnixNano":"1696487580000000000", "timeUnixNano":"1696512423758783158", "asDouble":3000 }, ... ``` Signed-off-by: ChrsMark --- .chloggen/cpuinfo_frequency_metric.yaml | 27 +++++++++ receiver/hostmetricsreceiver/go.mod | 2 +- .../scraper/cpuscraper/cpu_scraper.go | 17 ++++++ .../scraper/cpuscraper/cpu_scraper_linux.go | 22 +++++++ .../scraper/cpuscraper/cpu_scraper_others.go | 5 ++ .../scraper/cpuscraper/documentation.md | 14 +++++ .../internal/metadata/generated_config.go | 4 ++ .../metadata/generated_config_test.go | 2 + .../internal/metadata/generated_metrics.go | 59 +++++++++++++++++++ .../metadata/generated_metrics_test.go | 18 ++++++ .../internal/metadata/testdata/config.yaml | 4 ++ .../internal/scraper/cpuscraper/metadata.yaml | 8 +++ 12 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 .chloggen/cpuinfo_frequency_metric.yaml diff --git a/.chloggen/cpuinfo_frequency_metric.yaml b/.chloggen/cpuinfo_frequency_metric.yaml new file mode 100644 index 000000000000..cf44d87b8bf7 --- /dev/null +++ b/.chloggen/cpuinfo_frequency_metric.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: receiver/hostmetricsreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Added support for host's cpuinfo frequnecies. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [27445] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: In Linux the current frequency is populated using the values from /proc/cpuinfo. An os specific implementation will be needed for Windows and others. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] \ No newline at end of file diff --git a/receiver/hostmetricsreceiver/go.mod b/receiver/hostmetricsreceiver/go.mod index 2d181a07f031..a0488270cbde 100644 --- a/receiver/hostmetricsreceiver/go.mod +++ b/receiver/hostmetricsreceiver/go.mod @@ -8,6 +8,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.88.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.88.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.88.0 + github.com/prometheus/procfs v0.11.1 github.com/shirou/gopsutil/v3 v3.23.9 github.com/stretchr/testify v1.8.4 github.com/yusufpapurcu/wmi v1.2.3 @@ -79,7 +80,6 @@ require ( github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.11.1 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper.go index 5507247f960f..dbd1238c717a 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper.go @@ -5,6 +5,7 @@ package cpuscraper // import "github.com/open-telemetry/opentelemetry-collector- import ( "context" + "fmt" "time" "github.com/shirou/gopsutil/v3/common" @@ -21,6 +22,7 @@ import ( ) const metricsLen = 2 +const hzInAMHz = 1_000_000 // scraper for CPU Metrics type scraper struct { @@ -35,6 +37,11 @@ type scraper struct { now func() time.Time } +type cpuInfo struct { + frequency float64 + processor uint +} + // newCPUScraper creates a set of CPU related metrics func newCPUScraper(_ context.Context, settings receiver.CreateSettings, cfg *Config) *scraper { return &scraper{settings: settings, config: cfg, bootTime: host.BootTimeWithContext, times: cpu.TimesWithContext, ucal: &ucal.CPUUtilizationCalculator{}, now: time.Now} @@ -79,5 +86,15 @@ func (s *scraper) scrape(ctx context.Context) (pmetric.Metrics, error) { } s.mb.RecordSystemCPULogicalCountDataPoint(now, int64(numCPU)) + if s.config.MetricsBuilderConfig.Metrics.SystemCPUFrequency.Enabled { + cpuInfos, err := s.getCPUInfo() + if err != nil { + return pmetric.NewMetrics(), scrapererror.NewPartialScrapeError(err, metricsLen) + } + for _, cInfo := range cpuInfos { + s.mb.RecordSystemCPUFrequencyDataPoint(now, cInfo.frequency*hzInAMHz, fmt.Sprintf("cpu%d", cInfo.processor)) + } + } + return s.mb.Emit(), nil } diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_linux.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_linux.go index ee430e2db397..99517c8ea911 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_linux.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_linux.go @@ -7,8 +7,10 @@ package cpuscraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper" import ( + "github.com/prometheus/procfs" "github.com/shirou/gopsutil/v3/cpu" "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/receiver/scrapererror" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/ucal" @@ -35,3 +37,23 @@ func (s *scraper) recordCPUUtilization(now pcommon.Timestamp, cpuUtilization uca s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Steal, cpuUtilization.CPU, metadata.AttributeStateSteal) s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Iowait, cpuUtilization.CPU, metadata.AttributeStateWait) } + +func (s *scraper) getCPUInfo() ([]cpuInfo, error) { + var cpuInfos []cpuInfo + fs, err := procfs.NewDefaultFS() + if err != nil { + return nil, scrapererror.NewPartialScrapeError(err, metricsLen) + } + cInf, err := fs.CPUInfo() + if err != nil { + return nil, scrapererror.NewPartialScrapeError(err, metricsLen) + } + for _, cInfo := range cInf { + c := cpuInfo{ + frequency: cInfo.CPUMHz, + processor: cInfo.Processor, + } + cpuInfos = append(cpuInfos, c) + } + return cpuInfos, nil +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_others.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_others.go index 2c7d8d62469b..1cef0d5e5089 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_others.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/cpu_scraper_others.go @@ -27,3 +27,8 @@ func (s *scraper) recordCPUUtilization(now pcommon.Timestamp, cpuUtilization uca s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Idle, cpuUtilization.CPU, metadata.AttributeStateIdle) s.mb.RecordSystemCPUUtilizationDataPoint(now, cpuUtilization.Irq, cpuUtilization.CPU, metadata.AttributeStateInterrupt) } + +func (s *scraper) getCPUInfo() ([]cpuInfo, error) { + var cpuInfos []cpuInfo + return cpuInfos, nil +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/documentation.md b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/documentation.md index 61cec80e1b54..7fa1b7e05513 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/documentation.md +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/documentation.md @@ -39,6 +39,20 @@ metrics: enabled: true ``` +### system.cpu.frequency + +Current frequency of the CPU core in Hz. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| Hz | Gauge | Double | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| cpu | Logical CPU number starting at 0. | Any Str | + ### system.cpu.logical.count Number of available logical CPUs. diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go index ae1e59703b16..8cea0feef5e7 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go @@ -25,6 +25,7 @@ func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { // MetricsConfig provides config for hostmetricsreceiver/cpu metrics. type MetricsConfig struct { + SystemCPUFrequency MetricConfig `mapstructure:"system.cpu.frequency"` SystemCPULogicalCount MetricConfig `mapstructure:"system.cpu.logical.count"` SystemCPUPhysicalCount MetricConfig `mapstructure:"system.cpu.physical.count"` SystemCPUTime MetricConfig `mapstructure:"system.cpu.time"` @@ -33,6 +34,9 @@ type MetricsConfig struct { func DefaultMetricsConfig() MetricsConfig { return MetricsConfig{ + SystemCPUFrequency: MetricConfig{ + Enabled: false, + }, SystemCPULogicalCount: MetricConfig{ Enabled: false, }, diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go index 1bd8ffc8b026..20fdd234b2de 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go @@ -26,6 +26,7 @@ func TestMetricsBuilderConfig(t *testing.T) { name: "all_set", want: MetricsBuilderConfig{ Metrics: MetricsConfig{ + SystemCPUFrequency: MetricConfig{Enabled: true}, SystemCPULogicalCount: MetricConfig{Enabled: true}, SystemCPUPhysicalCount: MetricConfig{Enabled: true}, SystemCPUTime: MetricConfig{Enabled: true}, @@ -37,6 +38,7 @@ func TestMetricsBuilderConfig(t *testing.T) { name: "none_set", want: MetricsBuilderConfig{ Metrics: MetricsConfig{ + SystemCPUFrequency: MetricConfig{Enabled: false}, SystemCPULogicalCount: MetricConfig{Enabled: false}, SystemCPUPhysicalCount: MetricConfig{Enabled: false}, SystemCPUTime: MetricConfig{Enabled: false}, diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go index fa5eb13ebb96..d6eb80141269 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go @@ -62,6 +62,57 @@ var MapAttributeState = map[string]AttributeState{ "wait": AttributeStateWait, } +type metricSystemCPUFrequency struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills system.cpu.frequency metric with initial data. +func (m *metricSystemCPUFrequency) init() { + m.data.SetName("system.cpu.frequency") + m.data.SetDescription("Current frequency of the CPU core in Hz.") + m.data.SetUnit("Hz") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricSystemCPUFrequency) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64, cpuAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) + dp.Attributes().PutStr("cpu", cpuAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemCPUFrequency) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemCPUFrequency) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemCPUFrequency(cfg MetricConfig) metricSystemCPUFrequency { + m := metricSystemCPUFrequency{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + type metricSystemCPULogicalCount struct { data pmetric.Metric // data buffer for generated metric. config MetricConfig // metric config provided by user. @@ -278,6 +329,7 @@ type MetricsBuilder struct { metricsCapacity int // maximum observed number of metrics per resource. metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. buildInfo component.BuildInfo // contains version information. + metricSystemCPUFrequency metricSystemCPUFrequency metricSystemCPULogicalCount metricSystemCPULogicalCount metricSystemCPUPhysicalCount metricSystemCPUPhysicalCount metricSystemCPUTime metricSystemCPUTime @@ -300,6 +352,7 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSetting startTime: pcommon.NewTimestampFromTime(time.Now()), metricsBuffer: pmetric.NewMetrics(), buildInfo: settings.BuildInfo, + metricSystemCPUFrequency: newMetricSystemCPUFrequency(mbc.Metrics.SystemCPUFrequency), metricSystemCPULogicalCount: newMetricSystemCPULogicalCount(mbc.Metrics.SystemCPULogicalCount), metricSystemCPUPhysicalCount: newMetricSystemCPUPhysicalCount(mbc.Metrics.SystemCPUPhysicalCount), metricSystemCPUTime: newMetricSystemCPUTime(mbc.Metrics.SystemCPUTime), @@ -361,6 +414,7 @@ func (mb *MetricsBuilder) EmitForResource(rmo ...ResourceMetricsOption) { ils.Scope().SetName("otelcol/hostmetricsreceiver/cpu") ils.Scope().SetVersion(mb.buildInfo.Version) ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricSystemCPUFrequency.emit(ils.Metrics()) mb.metricSystemCPULogicalCount.emit(ils.Metrics()) mb.metricSystemCPUPhysicalCount.emit(ils.Metrics()) mb.metricSystemCPUTime.emit(ils.Metrics()) @@ -385,6 +439,11 @@ func (mb *MetricsBuilder) Emit(rmo ...ResourceMetricsOption) pmetric.Metrics { return metrics } +// RecordSystemCPUFrequencyDataPoint adds a data point to system.cpu.frequency metric. +func (mb *MetricsBuilder) RecordSystemCPUFrequencyDataPoint(ts pcommon.Timestamp, val float64, cpuAttributeValue string) { + mb.metricSystemCPUFrequency.recordDataPoint(mb.startTime, ts, val, cpuAttributeValue) +} + // RecordSystemCPULogicalCountDataPoint adds a data point to system.cpu.logical.count metric. func (mb *MetricsBuilder) RecordSystemCPULogicalCountDataPoint(ts pcommon.Timestamp, val int64) { mb.metricSystemCPULogicalCount.recordDataPoint(mb.startTime, ts, val) diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go index 98749fb0e0c4..1333e19db136 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go @@ -55,6 +55,9 @@ func TestMetricsBuilder(t *testing.T) { defaultMetricsCount := 0 allMetricsCount := 0 + allMetricsCount++ + mb.RecordSystemCPUFrequencyDataPoint(ts, 1, "cpu-val") + allMetricsCount++ mb.RecordSystemCPULogicalCountDataPoint(ts, 1) @@ -90,6 +93,21 @@ func TestMetricsBuilder(t *testing.T) { validatedMetrics := make(map[string]bool) for i := 0; i < ms.Len(); i++ { switch ms.At(i).Name() { + case "system.cpu.frequency": + assert.False(t, validatedMetrics["system.cpu.frequency"], "Found a duplicate in the metrics slice: system.cpu.frequency") + validatedMetrics["system.cpu.frequency"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Current frequency of the CPU core in Hz.", ms.At(i).Description()) + assert.Equal(t, "Hz", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + attrVal, ok := dp.Attributes().Get("cpu") + assert.True(t, ok) + assert.EqualValues(t, "cpu-val", attrVal.Str()) case "system.cpu.logical.count": assert.False(t, validatedMetrics["system.cpu.logical.count"], "Found a duplicate in the metrics slice: system.cpu.logical.count") validatedMetrics["system.cpu.logical.count"] = true diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml index a4f7f0de3adb..2dbaa6a35373 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml @@ -1,6 +1,8 @@ default: all_set: metrics: + system.cpu.frequency: + enabled: true system.cpu.logical.count: enabled: true system.cpu.physical.count: @@ -11,6 +13,8 @@ all_set: enabled: true none_set: metrics: + system.cpu.frequency: + enabled: false system.cpu.logical.count: enabled: false system.cpu.physical.count: diff --git a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/metadata.yaml b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/metadata.yaml index 17de7eb384a8..ad71d581a182 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/metadata.yaml +++ b/receiver/hostmetricsreceiver/internal/scraper/cpuscraper/metadata.yaml @@ -50,3 +50,11 @@ metrics: value_type: int monotonic: false aggregation_temporality: cumulative + + system.cpu.frequency: + enabled: false + description: Current frequency of the CPU core in Hz. + unit: "Hz" + gauge: + value_type: double + attributes: [cpu] \ No newline at end of file