From 6b5ef82c3d26f11078302b7c81196ea291863b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20NO=C3=8BL?= Date: Tue, 22 Oct 2024 14:34:39 +0200 Subject: [PATCH 1/2] add system uptime metric --- .chloggen/add-hostmetric-system-uptime.yaml | 25 +++ receiver/hostmetricsreceiver/README.md | 2 + receiver/hostmetricsreceiver/config_test.go | 6 + .../hostmetricsreceiver/example_config.yaml | 1 + receiver/hostmetricsreceiver/factory.go | 2 + .../hostmetrics_receiver_test.go | 15 ++ .../internal/scraper/uptimescraper/config.go | 16 ++ .../internal/scraper/uptimescraper/doc.go | 6 + .../scraper/uptimescraper/documentation.md | 23 ++ .../internal/scraper/uptimescraper/factory.go | 54 +++++ .../internal/metadata/generated_config.go | 50 +++++ .../metadata/generated_config_test.go | 59 +++++ .../internal/metadata/generated_metrics.go | 202 ++++++++++++++++++ .../metadata/generated_metrics_test.go | 106 +++++++++ .../internal/metadata/testdata/config.yaml | 9 + .../scraper/uptimescraper/metadata.yaml | 16 ++ .../scraper/uptimescraper/package_test.go | 14 ++ .../internal/scraper/uptimescraper/uptime.go | 59 +++++ .../scraper/uptimescraper/uptime_test.go | 50 +++++ .../hostmetricsreceiver/testdata/config.yaml | 1 + 20 files changed, 716 insertions(+) create mode 100644 .chloggen/add-hostmetric-system-uptime.yaml create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/config.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/doc.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/factory.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config_test.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/testdata/config.yaml create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/package_test.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime.go create mode 100644 receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go diff --git a/.chloggen/add-hostmetric-system-uptime.yaml b/.chloggen/add-hostmetric-system-uptime.yaml new file mode 100644 index 0000000000000..296b5a311edcd --- /dev/null +++ b/.chloggen/add-hostmetric-system-uptime.yaml @@ -0,0 +1,25 @@ +# 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: hostmetricsreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add an uptime scraper in the hostmetrics receiver + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [31627] + +# (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: + +# 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: [user] diff --git a/receiver/hostmetricsreceiver/README.md b/receiver/hostmetricsreceiver/README.md index 952d984c3b554..398b30a9ae1d6 100644 --- a/receiver/hostmetricsreceiver/README.md +++ b/receiver/hostmetricsreceiver/README.md @@ -49,6 +49,7 @@ The available scrapers are: | [paging] | All | Paging/Swap space utilization and I/O metrics | | [processes] | Linux, Mac | Process count metrics | | [process] | Linux, Windows, Mac | Per process CPU, Memory, and Disk I/O metrics | +| [uptime] | Linux, Windows, Mac | Uptime metric | [cpu]: ./internal/scraper/cpuscraper/documentation.md [disk]: ./internal/scraper/diskscraper/documentation.md @@ -59,6 +60,7 @@ The available scrapers are: [paging]: ./internal/scraper/pagingscraper/documentation.md [processes]: ./internal/scraper/processesscraper/documentation.md [process]: ./internal/scraper/processscraper/documentation.md +[uptime]: ./internal/scraper/uptimescraper/documentation.md ### Notes diff --git a/receiver/hostmetricsreceiver/config_test.go b/receiver/hostmetricsreceiver/config_test.go index fcb722db68177..2d00dab4b7382 100644 --- a/receiver/hostmetricsreceiver/config_test.go +++ b/receiver/hostmetricsreceiver/config_test.go @@ -27,6 +27,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/pagingscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processesscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processscraper" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" ) func TestLoadConfig(t *testing.T) { @@ -118,6 +119,11 @@ func TestLoadConfig(t *testing.T) { cfg.SetEnvMap(common.EnvMap{}) return cfg })(), + uptimescraper.TypeStr: (func() internal.Config { + cfg := (&uptimescraper.Factory{}).CreateDefaultConfig() + cfg.SetEnvMap(common.EnvMap{}) + return cfg + })(), }, } diff --git a/receiver/hostmetricsreceiver/example_config.yaml b/receiver/hostmetricsreceiver/example_config.yaml index 1c286253cfac1..d980696867736 100644 --- a/receiver/hostmetricsreceiver/example_config.yaml +++ b/receiver/hostmetricsreceiver/example_config.yaml @@ -14,6 +14,7 @@ receivers: network: paging: processes: + uptime: exporters: debug: diff --git a/receiver/hostmetricsreceiver/factory.go b/receiver/hostmetricsreceiver/factory.go index 81cfbd572bed4..227ef00862d16 100644 --- a/receiver/hostmetricsreceiver/factory.go +++ b/receiver/hostmetricsreceiver/factory.go @@ -27,6 +27,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/pagingscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processesscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processscraper" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" ) const ( @@ -45,6 +46,7 @@ var ( pagingscraper.TypeStr: &pagingscraper.Factory{}, processesscraper.TypeStr: &processesscraper.Factory{}, processscraper.TypeStr: &processscraper.Factory{}, + uptimescraper.TypeStr: &uptimescraper.Factory{}, } ) diff --git a/receiver/hostmetricsreceiver/hostmetrics_receiver_test.go b/receiver/hostmetricsreceiver/hostmetrics_receiver_test.go index 07f5b50926b66..6919a4fe5e2f8 100644 --- a/receiver/hostmetricsreceiver/hostmetrics_receiver_test.go +++ b/receiver/hostmetricsreceiver/hostmetrics_receiver_test.go @@ -34,6 +34,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/pagingscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processesscraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/processscraper" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" ) var allMetrics = []string{ @@ -81,6 +82,7 @@ var factories = map[string]internal.ScraperFactory{ pagingscraper.TypeStr: &pagingscraper.Factory{}, processesscraper.TypeStr: &processesscraper.Factory{}, processscraper.TypeStr: &processscraper.Factory{}, + uptimescraper.TypeStr: &uptimescraper.Factory{}, } type testEnv struct { @@ -395,6 +397,19 @@ func Benchmark_ScrapeProcessMetrics(b *testing.B) { benchmarkScrapeMetrics(b, cfg) } +func Benchmark_ScrapeUptimeMetrics(b *testing.B) { + if runtime.GOOS != "linux" && runtime.GOOS != "windows" { + b.Skip("skipping test on non linux/windows") + } + + cfg := &Config{ + ControllerConfig: scraperhelper.NewDefaultControllerConfig(), + Scrapers: map[string]internal.Config{uptimescraper.TypeStr: (&uptimescraper.Factory{}).CreateDefaultConfig()}, + } + + benchmarkScrapeMetrics(b, cfg) +} + func Benchmark_ScrapeSystemMetrics(b *testing.B) { cfg := &Config{ ControllerConfig: scraperhelper.NewDefaultControllerConfig(), diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/config.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/config.go new file mode 100644 index 0000000000000..00bdcff6722d2 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/config.go @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package uptimescraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" + +import ( + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata" +) + +// Config relating to Uptime Metric Scraper. +type Config struct { + // MetricsBuilderConfig allows to customize scraped metrics/attributes representation. + metadata.MetricsBuilderConfig `mapstructure:",squash"` + internal.ScraperConfig +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/doc.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/doc.go new file mode 100644 index 0000000000000..256a95de192a5 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +package uptimescraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md new file mode 100644 index 0000000000000..eb7fb94224ac6 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md @@ -0,0 +1,23 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# hostmetricsreceiver/uptime + +**Parent Component:** hostmetrics + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### system.uptime + +The time the system has been running + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| s | Sum | Double | Cumulative | false | diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/factory.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/factory.go new file mode 100644 index 0000000000000..9f957c33b2fb1 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/factory.go @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package uptimescraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" + +import ( + "context" + "errors" + "runtime" + + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/scraperhelper" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal" + hostmeta "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/metadata" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata" +) + +// This file implements Factory for Uptime scraper. + +const ( + // TypeStr the value of "type" key in configuration. + TypeStr = "uptime" +) + +// Factory is the Factory for scraper. +type Factory struct { +} + +// CreateDefaultConfig creates the default configuration for the Scraper. +func (f *Factory) CreateDefaultConfig() internal.Config { + return &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } +} + +// CreateMetricsScraper creates a resource scraper based on provided config. +func (f *Factory) CreateMetricsScraper( + ctx context.Context, + settings receiver.Settings, + cfg internal.Config, +) (scraperhelper.Scraper, error) { + if runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + return nil, errors.New("uptime scraper only available on Linux, Windows, or MacOS") + } + + uptimeScraper := newUptimeScraper(ctx, settings, cfg.(*Config)) + + return scraperhelper.NewScraper( + hostmeta.Type, + uptimeScraper.scrape, + scraperhelper.WithStart(uptimeScraper.start), + ) +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config.go new file mode 100644 index 0000000000000..f41b077ee3340 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config.go @@ -0,0 +1,50 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/confmap" +) + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for hostmetricsreceiver/uptime metrics. +type MetricsConfig struct { + SystemUptime MetricConfig `mapstructure:"system.uptime"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + SystemUptime: MetricConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for hostmetricsreceiver/uptime metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + } +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config_test.go new file mode 100644 index 0000000000000..f1475ffd1053a --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_config_test.go @@ -0,0 +1,59 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemUptime: MetricConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemUptime: MetricConfig{Enabled: false}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{})); diff != "" { + t.Errorf("Config mismatch (-expected +actual):\n%s", diff) + } + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go new file mode 100644 index 0000000000000..4d091033911d8 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go @@ -0,0 +1,202 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" + conventions "go.opentelemetry.io/collector/semconv/v1.9.0" +) + +type metricSystemUptime 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.uptime metric with initial data. +func (m *metricSystemUptime) init() { + m.data.SetName("system.uptime") + m.data.SetDescription("The time the system has been running") + m.data.SetUnit("s") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) +} + +func (m *metricSystemUptime) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemUptime) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemUptime) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemUptime(cfg MetricConfig) metricSystemUptime { + m := metricSystemUptime{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + metricSystemUptime metricSystemUptime +} + +// MetricBuilderOption applies changes to default metrics builder. +type MetricBuilderOption interface { + apply(*MetricsBuilder) +} + +type metricBuilderOptionFunc func(mb *MetricsBuilder) + +func (mbof metricBuilderOptionFunc) apply(mb *MetricsBuilder) { + mbof(mb) +} + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { + return metricBuilderOptionFunc(func(mb *MetricsBuilder) { + mb.startTime = startTime + }) +} + +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, options ...MetricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricSystemUptime: newMetricSystemUptime(mbc.Metrics.SystemUptime), + } + + for _, op := range options { + op.apply(mb) + } + return mb +} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption interface { + apply(pmetric.ResourceMetrics) +} + +type resourceMetricsOptionFunc func(pmetric.ResourceMetrics) + +func (rmof resourceMetricsOptionFunc) apply(rm pmetric.ResourceMetrics) { + rmof(rm) +} + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + }) +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + }) +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + rm.SetSchemaUrl(conventions.SchemaURL) + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName("github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper") + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricSystemUptime.emit(ils.Metrics()) + + for _, op := range options { + op.apply(rm) + } + + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(options...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordSystemUptimeDataPoint adds a data point to system.uptime metric. +func (mb *MetricsBuilder) RecordSystemUptimeDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricSystemUptime.recordDataPoint(mb.startTime, ts, val) +} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...MetricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op.apply(mb) + } +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go new file mode 100644 index 0000000000000..917f1c71674ad --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go @@ -0,0 +1,106 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +type testDataSet int + +const ( + testDataSetDefault testDataSet = iota + testDataSetAll + testDataSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + metricsSet testDataSet + resAttrsSet testDataSet + expectEmpty bool + }{ + { + name: "default", + }, + { + name: "all_set", + metricsSet: testDataSetAll, + resAttrsSet: testDataSetAll, + }, + { + name: "none_set", + metricsSet: testDataSetNone, + resAttrsSet: testDataSetNone, + expectEmpty: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := receivertest.NewNopSettings() + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemUptimeDataPoint(ts, 1) + + res := pcommon.NewResource() + metrics := mb.Emit(WithResource(res)) + + if tt.expectEmpty { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if tt.metricsSet == testDataSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if tt.metricsSet == testDataSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "system.uptime": + assert.False(t, validatedMetrics["system.uptime"], "Found a duplicate in the metrics slice: system.uptime") + validatedMetrics["system.uptime"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "The time the system has been running", ms.At(i).Description()) + assert.Equal(t, "s", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.InDelta(t, float64(1), dp.DoubleValue(), 0.01) + } + } + }) + } +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/testdata/config.yaml b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/testdata/config.yaml new file mode 100644 index 0000000000000..d4dcbfe4cf60f --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/testdata/config.yaml @@ -0,0 +1,9 @@ +default: +all_set: + metrics: + system.uptime: + enabled: true +none_set: + metrics: + system.uptime: + enabled: false diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml new file mode 100644 index 0000000000000..c84449b04fab4 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml @@ -0,0 +1,16 @@ +type: hostmetricsreceiver/uptime + +parent: hostmetrics + +sem_conv_version: 1.9.0 + +metrics: + system.uptime: + enabled: true + description: The time the system has been running + unit: s + sum: + value_type: double + aggregation_temporality: cumulative + monotonic: false + attributes: [] diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/package_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/package_test.go new file mode 100644 index 0000000000000..6de26a8152d0c --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package uptimescraper + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime.go new file mode 100644 index 0000000000000..db6d307c53c56 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package uptimescraper // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper" + +import ( + "context" + "time" + + "github.com/shirou/gopsutil/v4/common" + "github.com/shirou/gopsutil/v4/host" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata" +) + +// scraper for Uptime Metrics +type scraper struct { + settings receiver.Settings + config *Config + mb *metadata.MetricsBuilder + + // for mocking + bootTime func(context.Context) (uint64, error) + uptime func(context.Context) (uint64, error) +} + +// newUptimeScraper creates an Uptime related metric +func newUptimeScraper(_ context.Context, settings receiver.Settings, cfg *Config) *scraper { + return &scraper{settings: settings, config: cfg, bootTime: host.BootTimeWithContext, uptime: host.UptimeWithContext} +} + +func (s *scraper) start(ctx context.Context, _ component.Host) error { + ctx = context.WithValue(ctx, common.EnvKey, s.config.EnvMap) + bootTime, err := s.bootTime(ctx) + if err != nil { + return err + } + + s.mb = metadata.NewMetricsBuilder(s.config.MetricsBuilderConfig, s.settings, metadata.WithStartTime(pcommon.Timestamp(bootTime*1e9))) + return nil +} + +func (s *scraper) scrape(ctx context.Context) (pmetric.Metrics, error) { + now := pcommon.NewTimestampFromTime(time.Now()) + ctx = context.WithValue(ctx, common.EnvKey, s.config.EnvMap) + + uptime, err := s.uptime(ctx) + if err != nil { + return pmetric.NewMetrics(), err + } + + s.mb.RecordSystemUptimeDataPoint(now, float64(uptime)) + + return s.mb.Emit(), nil +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go new file mode 100644 index 0000000000000..a7c1e6d1224b0 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package uptimescraper + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata" +) + +func TestScrape(t *testing.T) { + ctx := context.Background() + fakeDate := time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC) + + s := newUptimeScraper(ctx, receivertest.NewNopSettings(), &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + }) + + // mock + s.bootTime = func(_ context.Context) (uint64, error) { + return uint64(fakeDate.Unix()), nil + } + s.uptime = func(_ context.Context) (uint64, error) { + return uint64(123456), nil + } + + require.NoError(t, s.start(ctx, componenttest.NewNopHost())) + + metrics, err := s.scrape(ctx) + require.NoErrorf(t, err, "scrape error %+v", err) + + assert.Equal(t, 1, metrics.MetricCount()) + assert.Equal(t, 1, metrics.DataPointCount()) + + metric := metrics.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0) + assert.Equal(t, pmetric.MetricTypeSum, metric.Type()) + + dataPoint := metric.Sum().DataPoints().At(0) + assert.Equal(t, float64(123456), dataPoint.DoubleValue()) + assert.Equal(t, fakeDate, dataPoint.StartTimestamp().AsTime()) +} diff --git a/receiver/hostmetricsreceiver/testdata/config.yaml b/receiver/hostmetricsreceiver/testdata/config.yaml index c8c2e71b16317..2128c3dcbc41e 100644 --- a/receiver/hostmetricsreceiver/testdata/config.yaml +++ b/receiver/hostmetricsreceiver/testdata/config.yaml @@ -21,6 +21,7 @@ receivers: include: names: ["test2", "test3"] match_type: "regexp" + uptime: processors: nop: From 5d9b9bb9aa542daf3d6b6b06780a328b6aa62ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20NO=C3=8BL?= Date: Mon, 28 Oct 2024 11:38:58 +0100 Subject: [PATCH 2/2] switch system.uptime to gauge according to semantic convention, see discussion in: https://github.com/open-telemetry/semantic-conventions/pull/1507 --- .../internal/scraper/uptimescraper/documentation.md | 6 +++--- .../internal/metadata/generated_metrics.go | 12 +++++------- .../internal/metadata/generated_metrics_test.go | 8 +++----- .../internal/scraper/uptimescraper/metadata.yaml | 4 +--- .../internal/scraper/uptimescraper/uptime_test.go | 4 ++-- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md index eb7fb94224ac6..ce38e605d2914 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/documentation.md @@ -18,6 +18,6 @@ metrics: The time the system has been running -| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | -| ---- | ----------- | ---------- | ----------------------- | --------- | -| s | Sum | Double | Cumulative | false | +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| s | Gauge | Double | diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go index 4d091033911d8..b586215338e1e 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics.go @@ -23,16 +23,14 @@ func (m *metricSystemUptime) init() { m.data.SetName("system.uptime") m.data.SetDescription("The time the system has been running") m.data.SetUnit("s") - m.data.SetEmptySum() - m.data.Sum().SetIsMonotonic(false) - m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + m.data.SetEmptyGauge() } func (m *metricSystemUptime) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64) { if !m.config.Enabled { return } - dp := m.data.Sum().DataPoints().AppendEmpty() + dp := m.data.Gauge().DataPoints().AppendEmpty() dp.SetStartTimestamp(start) dp.SetTimestamp(ts) dp.SetDoubleValue(val) @@ -40,14 +38,14 @@ func (m *metricSystemUptime) recordDataPoint(start pcommon.Timestamp, ts pcommon // updateCapacity saves max length of data point slices that will be used for the slice capacity. func (m *metricSystemUptime) updateCapacity() { - if m.data.Sum().DataPoints().Len() > m.capacity { - m.capacity = m.data.Sum().DataPoints().Len() + 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 *metricSystemUptime) emit(metrics pmetric.MetricSlice) { - if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { m.updateCapacity() m.data.MoveTo(metrics.AppendEmpty()) m.init() diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go index 917f1c71674ad..472d2df85ea1a 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/internal/metadata/generated_metrics_test.go @@ -88,13 +88,11 @@ func TestMetricsBuilder(t *testing.T) { case "system.uptime": assert.False(t, validatedMetrics["system.uptime"], "Found a duplicate in the metrics slice: system.uptime") validatedMetrics["system.uptime"] = true - assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) - assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) assert.Equal(t, "The time the system has been running", ms.At(i).Description()) assert.Equal(t, "s", ms.At(i).Unit()) - assert.False(t, ms.At(i).Sum().IsMonotonic()) - assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) - dp := ms.At(i).Sum().DataPoints().At(0) + 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()) diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml index c84449b04fab4..311077a1af946 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/metadata.yaml @@ -9,8 +9,6 @@ metrics: enabled: true description: The time the system has been running unit: s - sum: + gauge: value_type: double - aggregation_temporality: cumulative - monotonic: false attributes: [] diff --git a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go index a7c1e6d1224b0..1ceeebdd117cc 100644 --- a/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go +++ b/receiver/hostmetricsreceiver/internal/scraper/uptimescraper/uptime_test.go @@ -42,9 +42,9 @@ func TestScrape(t *testing.T) { assert.Equal(t, 1, metrics.DataPointCount()) metric := metrics.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0) - assert.Equal(t, pmetric.MetricTypeSum, metric.Type()) + assert.Equalf(t, pmetric.MetricTypeGauge, metric.Type(), "invalid metric type: %v", metric.Type()) - dataPoint := metric.Sum().DataPoints().At(0) + dataPoint := metric.Gauge().DataPoints().At(0) assert.Equal(t, float64(123456), dataPoint.DoubleValue()) assert.Equal(t, fakeDate, dataPoint.StartTimestamp().AsTime()) }