diff --git a/.chloggen/gbbr_dockerstats-container.uptime.yaml b/.chloggen/gbbr_dockerstats-container.uptime.yaml new file mode 100755 index 000000000000..74db53739de7 --- /dev/null +++ b/.chloggen/gbbr_dockerstats-container.uptime.yaml @@ -0,0 +1,20 @@ +# Use this changelog template to create an entry for release notes. +# If your change doesn't affect end users, such as a test fix or a tooling change, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. + +# 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: dockerstatsreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add container.uptime metric, indicating time elapsed since the start of the container. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [22037] + +# (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: diff --git a/receiver/dockerstatsreceiver/documentation.md b/receiver/dockerstatsreceiver/documentation.md index 8a6f863e4a2c..3927a1ef54c5 100644 --- a/receiver/dockerstatsreceiver/documentation.md +++ b/receiver/dockerstatsreceiver/documentation.md @@ -686,6 +686,14 @@ It requires docker API 1.23 or higher and kernel version >= 4.3 with pids cgroup | ---- | ----------- | ---------- | ----------------------- | --------- | | {pids} | Sum | Int | Cumulative | false | +### container.uptime + +Time elapsed since container start time. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| s | Gauge | Double | + ## Resource Attributes | Name | Description | Values | Enabled | diff --git a/receiver/dockerstatsreceiver/internal/metadata/generated_config.go b/receiver/dockerstatsreceiver/internal/metadata/generated_config.go index 4a2dd713eb50..65ad7e349a2f 100644 --- a/receiver/dockerstatsreceiver/internal/metadata/generated_config.go +++ b/receiver/dockerstatsreceiver/internal/metadata/generated_config.go @@ -89,6 +89,7 @@ type MetricsConfig struct { ContainerNetworkIoUsageTxPackets MetricConfig `mapstructure:"container.network.io.usage.tx_packets"` ContainerPidsCount MetricConfig `mapstructure:"container.pids.count"` ContainerPidsLimit MetricConfig `mapstructure:"container.pids.limit"` + ContainerUptime MetricConfig `mapstructure:"container.uptime"` } func DefaultMetricsConfig() MetricsConfig { @@ -285,6 +286,9 @@ func DefaultMetricsConfig() MetricsConfig { ContainerPidsLimit: MetricConfig{ Enabled: false, }, + ContainerUptime: MetricConfig{ + Enabled: false, + }, } } diff --git a/receiver/dockerstatsreceiver/internal/metadata/generated_config_test.go b/receiver/dockerstatsreceiver/internal/metadata/generated_config_test.go index fd676b7be8ad..fdbd7970c855 100644 --- a/receiver/dockerstatsreceiver/internal/metadata/generated_config_test.go +++ b/receiver/dockerstatsreceiver/internal/metadata/generated_config_test.go @@ -90,6 +90,7 @@ func TestMetricsBuilderConfig(t *testing.T) { ContainerNetworkIoUsageTxPackets: MetricConfig{Enabled: true}, ContainerPidsCount: MetricConfig{Enabled: true}, ContainerPidsLimit: MetricConfig{Enabled: true}, + ContainerUptime: MetricConfig{Enabled: true}, }, ResourceAttributes: ResourceAttributesConfig{ ContainerHostname: ResourceAttributeConfig{Enabled: true}, @@ -168,6 +169,7 @@ func TestMetricsBuilderConfig(t *testing.T) { ContainerNetworkIoUsageTxPackets: MetricConfig{Enabled: false}, ContainerPidsCount: MetricConfig{Enabled: false}, ContainerPidsLimit: MetricConfig{Enabled: false}, + ContainerUptime: MetricConfig{Enabled: false}, }, ResourceAttributes: ResourceAttributesConfig{ ContainerHostname: ResourceAttributeConfig{Enabled: false}, diff --git a/receiver/dockerstatsreceiver/internal/metadata/generated_metrics.go b/receiver/dockerstatsreceiver/internal/metadata/generated_metrics.go index dd4f00719f82..8931a9b15abb 100644 --- a/receiver/dockerstatsreceiver/internal/metadata/generated_metrics.go +++ b/receiver/dockerstatsreceiver/internal/metadata/generated_metrics.go @@ -3320,6 +3320,55 @@ func newMetricContainerPidsLimit(cfg MetricConfig) metricContainerPidsLimit { return m } +type metricContainerUptime 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 container.uptime metric with initial data. +func (m *metricContainerUptime) init() { + m.data.SetName("container.uptime") + m.data.SetDescription("Time elapsed since container start time.") + m.data.SetUnit("s") + m.data.SetEmptyGauge() +} + +func (m *metricContainerUptime) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().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 *metricContainerUptime) 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 *metricContainerUptime) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricContainerUptime(cfg MetricConfig) metricContainerUptime { + m := metricContainerUptime{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 { @@ -3393,6 +3442,7 @@ type MetricsBuilder struct { metricContainerNetworkIoUsageTxPackets metricContainerNetworkIoUsageTxPackets metricContainerPidsCount metricContainerPidsCount metricContainerPidsLimit metricContainerPidsLimit + metricContainerUptime metricContainerUptime } // metricBuilderOption applies changes to default metrics builder. @@ -3481,6 +3531,7 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSetting metricContainerNetworkIoUsageTxPackets: newMetricContainerNetworkIoUsageTxPackets(mbc.Metrics.ContainerNetworkIoUsageTxPackets), metricContainerPidsCount: newMetricContainerPidsCount(mbc.Metrics.ContainerPidsCount), metricContainerPidsLimit: newMetricContainerPidsLimit(mbc.Metrics.ContainerPidsLimit), + metricContainerUptime: newMetricContainerUptime(mbc.Metrics.ContainerUptime), } for _, op := range options { op(mb) @@ -3643,6 +3694,7 @@ func (mb *MetricsBuilder) EmitForResource(rmo ...ResourceMetricsOption) { mb.metricContainerNetworkIoUsageTxPackets.emit(ils.Metrics()) mb.metricContainerPidsCount.emit(ils.Metrics()) mb.metricContainerPidsLimit.emit(ils.Metrics()) + mb.metricContainerUptime.emit(ils.Metrics()) for _, op := range rmo { op(mb.resourceAttributesConfig, rm) @@ -3983,6 +4035,11 @@ func (mb *MetricsBuilder) RecordContainerPidsLimitDataPoint(ts pcommon.Timestamp mb.metricContainerPidsLimit.recordDataPoint(mb.startTime, ts, val) } +// RecordContainerUptimeDataPoint adds a data point to container.uptime metric. +func (mb *MetricsBuilder) RecordContainerUptimeDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricContainerUptime.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) { diff --git a/receiver/dockerstatsreceiver/internal/metadata/generated_metrics_test.go b/receiver/dockerstatsreceiver/internal/metadata/generated_metrics_test.go index 957d8f66a648..cdff66efb67d 100644 --- a/receiver/dockerstatsreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/dockerstatsreceiver/internal/metadata/generated_metrics_test.go @@ -267,6 +267,9 @@ func TestMetricsBuilder(t *testing.T) { allMetricsCount++ mb.RecordContainerPidsLimitDataPoint(ts, 1) + allMetricsCount++ + mb.RecordContainerUptimeDataPoint(ts, 1) + metrics := mb.Emit(WithContainerHostname("attr-val"), WithContainerID("attr-val"), WithContainerImageName("attr-val"), WithContainerName("attr-val"), WithContainerRuntime("attr-val")) if test.configSet == testSetNone { @@ -1316,6 +1319,18 @@ func TestMetricsBuilder(t *testing.T) { assert.Equal(t, ts, dp.Timestamp()) assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) assert.Equal(t, int64(1), dp.IntValue()) + case "container.uptime": + assert.False(t, validatedMetrics["container.uptime"], "Found a duplicate in the metrics slice: container.uptime") + validatedMetrics["container.uptime"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Time elapsed since container start time.", ms.At(i).Description()) + assert.Equal(t, "s", 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()) } } }) diff --git a/receiver/dockerstatsreceiver/internal/metadata/testdata/config.yaml b/receiver/dockerstatsreceiver/internal/metadata/testdata/config.yaml index 6ed548cf19b3..a75550ddcc0e 100644 --- a/receiver/dockerstatsreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/dockerstatsreceiver/internal/metadata/testdata/config.yaml @@ -129,6 +129,8 @@ all_set: enabled: true container.pids.limit: enabled: true + container.uptime: + enabled: true resource_attributes: container.hostname: enabled: true @@ -270,6 +272,8 @@ none_set: enabled: false container.pids.limit: enabled: false + container.uptime: + enabled: false resource_attributes: container.hostname: enabled: false diff --git a/receiver/dockerstatsreceiver/metadata.yaml b/receiver/dockerstatsreceiver/metadata.yaml index 9929ffce23c5..4794db63672a 100644 --- a/receiver/dockerstatsreceiver/metadata.yaml +++ b/receiver/dockerstatsreceiver/metadata.yaml @@ -649,3 +649,11 @@ metrics: value_type: int aggregation: cumulative monotonic: false + + # Base + container.uptime: + enabled: false + description: "Time elapsed since container start time." + unit: s + gauge: + value_type: double diff --git a/receiver/dockerstatsreceiver/receiver.go b/receiver/dockerstatsreceiver/receiver.go index 4d2bbc895e2f..b0f941fc0252 100644 --- a/receiver/dockerstatsreceiver/receiver.go +++ b/receiver/dockerstatsreceiver/receiver.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/docker/docker/api/types" dtypes "github.com/docker/docker/api/types" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/pdata/pcommon" @@ -104,18 +105,24 @@ func (r *receiver) scrapeV2(ctx context.Context) (pmetric.Metrics, error) { errs = multierr.Append(errs, scrapererror.NewPartialScrapeError(res.err, 0)) continue } - r.recordContainerStats(now, res.stats, res.container) + if err := r.recordContainerStats(now, res.stats, res.container); err != nil { + errs = multierr.Append(errs, err) + } } return r.mb.Emit(), errs } -func (r *receiver) recordContainerStats(now pcommon.Timestamp, containerStats *dtypes.StatsJSON, container *docker.Container) { +func (r *receiver) recordContainerStats(now pcommon.Timestamp, containerStats *dtypes.StatsJSON, container *docker.Container) error { + var errs error r.recordCPUMetrics(now, &containerStats.CPUStats, &containerStats.PreCPUStats) r.recordMemoryMetrics(now, &containerStats.MemoryStats) r.recordBlkioMetrics(now, &containerStats.BlkioStats) r.recordNetworkMetrics(now, &containerStats.Networks) r.recordPidsMetrics(now, &containerStats.PidsStats) + if err := r.recordBaseMetrics(now, container.ContainerJSONBase); err != nil { + errs = multierr.Append(errs, err) + } // Always-present resource attrs + the user-configured resource attrs resourceCapacity := defaultResourcesLen + len(r.config.EnvVarsToMetricLabels) + len(r.config.ContainerLabelsToMetricLabels) @@ -145,6 +152,7 @@ func (r *receiver) recordContainerStats(now pcommon.Timestamp, containerStats *d } r.mb.EmitForResource(resourceMetricsOptions...) + return errs } func (r *receiver) recordMemoryMetrics(now pcommon.Timestamp, memoryStats *dtypes.MemoryStats) { @@ -265,3 +273,15 @@ func (r *receiver) recordPidsMetrics(now pcommon.Timestamp, pidsStats *dtypes.Pi } } } + +func (r *receiver) recordBaseMetrics(now pcommon.Timestamp, base *types.ContainerJSONBase) error { + t, err := time.Parse(time.RFC3339, base.State.StartedAt) + if err != nil { + // value not available or invalid + return scrapererror.NewPartialScrapeError(fmt.Errorf("error retrieving container.uptime from Container.State.StartedAt: %w", err), 1) + } + if v := now.AsTime().Sub(t); v > 0 { + r.mb.RecordContainerUptimeDataPoint(now, v.Seconds()) + } + return nil +} diff --git a/receiver/dockerstatsreceiver/receiver_test.go b/receiver/dockerstatsreceiver/receiver_test.go index b33c351a85c9..b28a079f2d28 100644 --- a/receiver/dockerstatsreceiver/receiver_test.go +++ b/receiver/dockerstatsreceiver/receiver_test.go @@ -17,9 +17,11 @@ import ( "testing" "time" + "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/receiver/receivertest" "go.opentelemetry.io/collector/receiver/scraperhelper" @@ -96,6 +98,7 @@ var ( ContainerNetworkIoUsageTxPackets: metricEnabled, ContainerPidsCount: metricEnabled, ContainerPidsLimit: metricEnabled, + ContainerUptime: metricEnabled, } ) @@ -254,11 +257,56 @@ func TestScrapeV2(t *testing.T) { assert.NoError(t, err) assert.NoError(t, pmetrictest.CompareMetrics(expectedMetrics, actualMetrics, pmetrictest.IgnoreMetricDataPointsOrder(), - pmetrictest.IgnoreResourceMetricsOrder(), pmetrictest.IgnoreStartTimestamp(), pmetrictest.IgnoreTimestamp())) + pmetrictest.IgnoreResourceMetricsOrder(), + pmetrictest.IgnoreStartTimestamp(), + pmetrictest.IgnoreTimestamp(), + pmetrictest.IgnoreMetricValues( + "container.uptime", // value depends on time.Now(), making it unpredictable as far as tests go + ), + )) }) } } +func TestRecordBaseMetrics(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.MetricsBuilderConfig.Metrics = metadata.MetricsConfig{ + ContainerUptime: metricEnabled, + } + r := newReceiver(receivertest.NewNopCreateSettings(), cfg) + now := time.Now() + started := now.Add(-2 * time.Second).Format(time.RFC3339) + + t.Run("ok", func(t *testing.T) { + err := r.recordBaseMetrics( + pcommon.NewTimestampFromTime(now), + &types.ContainerJSONBase{ + State: &types.ContainerState{ + StartedAt: started, + }, + }, + ) + require.NoError(t, err) + m := r.mb.Emit().ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0) + assert.Equal(t, "container.uptime", m.Name()) + dp := m.Gauge().DataPoints() + assert.Equal(t, 1, dp.Len()) + assert.Equal(t, 2, int(dp.At(0).DoubleValue())) + }) + + t.Run("error", func(t *testing.T) { + err := r.recordBaseMetrics( + pcommon.NewTimestampFromTime(now), + &types.ContainerJSONBase{ + State: &types.ContainerState{ + StartedAt: "bad date", + }, + }, + ) + require.Error(t, err) + }) +} + func dockerMockServer(urlToFile *map[string]string) (*httptest.Server, error) { urlToFileContents := make(map[string][]byte, len(*urlToFile)) for urlPath, filePath := range *urlToFile { diff --git a/receiver/dockerstatsreceiver/testdata/mock/cgroups_v2/expected_metrics.yaml b/receiver/dockerstatsreceiver/testdata/mock/cgroups_v2/expected_metrics.yaml index 91e22ab4586a..cdd603cc1aac 100644 --- a/receiver/dockerstatsreceiver/testdata/mock/cgroups_v2/expected_metrics.yaml +++ b/receiver/dockerstatsreceiver/testdata/mock/cgroups_v2/expected_metrics.yaml @@ -360,6 +360,13 @@ resourceMetrics: startTimeUnixNano: "1682426015940992000" timeUnixNano: "1682426015943175000" unit: '{pids}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest diff --git a/receiver/dockerstatsreceiver/testdata/mock/no_pids_stats/expected_metrics.yaml b/receiver/dockerstatsreceiver/testdata/mock/no_pids_stats/expected_metrics.yaml index 6008bab43a26..348212195c86 100644 --- a/receiver/dockerstatsreceiver/testdata/mock/no_pids_stats/expected_metrics.yaml +++ b/receiver/dockerstatsreceiver/testdata/mock/no_pids_stats/expected_metrics.yaml @@ -779,6 +779,13 @@ resourceMetrics: timeUnixNano: "1683723817613281000" isMonotonic: true unit: '{packets}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest diff --git a/receiver/dockerstatsreceiver/testdata/mock/pids_stats_max/expected_metrics.yaml b/receiver/dockerstatsreceiver/testdata/mock/pids_stats_max/expected_metrics.yaml index 1d61d33da7e1..2fdfccad4911 100644 --- a/receiver/dockerstatsreceiver/testdata/mock/pids_stats_max/expected_metrics.yaml +++ b/receiver/dockerstatsreceiver/testdata/mock/pids_stats_max/expected_metrics.yaml @@ -412,6 +412,13 @@ resourceMetrics: startTimeUnixNano: "1683723781127718000" timeUnixNano: "1683723781130612000" unit: '{pids}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest diff --git a/receiver/dockerstatsreceiver/testdata/mock/single_container/expected_metrics.yaml b/receiver/dockerstatsreceiver/testdata/mock/single_container/expected_metrics.yaml index 0ee640ef6af4..df4e91718235 100644 --- a/receiver/dockerstatsreceiver/testdata/mock/single_container/expected_metrics.yaml +++ b/receiver/dockerstatsreceiver/testdata/mock/single_container/expected_metrics.yaml @@ -733,6 +733,13 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1657771705535206000" unit: '{pids}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest diff --git a/receiver/dockerstatsreceiver/testdata/mock/two_containers/expected_metrics.yaml b/receiver/dockerstatsreceiver/testdata/mock/two_containers/expected_metrics.yaml index 410cdc538591..e0e8459b0489 100644 --- a/receiver/dockerstatsreceiver/testdata/mock/two_containers/expected_metrics.yaml +++ b/receiver/dockerstatsreceiver/testdata/mock/two_containers/expected_metrics.yaml @@ -685,6 +685,13 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1657771832637112000" unit: '{pids}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest @@ -1374,6 +1381,13 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1657771832637093000" unit: '{pids}' + - description: Time elapsed since container start time. + name: container.uptime + gauge: + dataPoints: + - asDouble: 0.0002888012543185477 + timeUnixNano: "1657771705535206000" + unit: 's' scope: name: otelcol/dockerstatsreceiver version: latest