From 64ebfbce662f261b3afbf6bef7d5d6c7ee80de69 Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Tue, 27 Jun 2023 09:43:04 -0700 Subject: [PATCH] Switch k8s.pod and k8s.container metrics to use pdata. (#23441) Updates https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/4367 --- .chloggen/switchk8spod.yaml | 11 + receiver/k8sclusterreceiver/e2e_test.go | 11 +- .../internal/collection/collector.go | 2 +- .../internal/container/containers.go | 184 ++--- .../internal/container/doc.go | 6 + .../internal/container/documentation.md | 107 +++ .../internal/metadata/generated_config.go | 136 +++ .../metadata/generated_config_test.go | 100 +++ .../internal/metadata/generated_metrics.go | 773 ++++++++++++++++++ .../metadata/generated_metrics_test.go | 309 +++++++ .../internal/metadata/testdata/config.yaml | 83 ++ .../internal/container/metadata.yaml | 103 +++ .../k8sclusterreceiver/internal/pod/doc.go | 6 + .../internal/pod/documentation.md | 31 + .../pod/internal/metadata/generated_config.go | 84 ++ .../metadata/generated_config_test.go | 74 ++ .../internal/metadata/generated_metrics.go | 224 +++++ .../metadata/generated_metrics_test.go | 137 ++++ .../internal/metadata/testdata/config.yaml | 31 + .../internal/pod/metadata.yaml | 37 + .../k8sclusterreceiver/internal/pod/pods.go | 106 +-- .../internal/pod/pods_test.go | 75 +- .../internal/pod/testdata/expected.yaml | 87 ++ .../testdata/e2e/expected.yaml | 120 ++- 24 files changed, 2554 insertions(+), 283 deletions(-) create mode 100755 .chloggen/switchk8spod.yaml create mode 100644 receiver/k8sclusterreceiver/internal/container/doc.go create mode 100644 receiver/k8sclusterreceiver/internal/container/documentation.md create mode 100644 receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config.go create mode 100644 receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config_test.go create mode 100644 receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics.go create mode 100644 receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics_test.go create mode 100644 receiver/k8sclusterreceiver/internal/container/internal/metadata/testdata/config.yaml create mode 100644 receiver/k8sclusterreceiver/internal/container/metadata.yaml create mode 100644 receiver/k8sclusterreceiver/internal/pod/doc.go create mode 100644 receiver/k8sclusterreceiver/internal/pod/documentation.md create mode 100644 receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config.go create mode 100644 receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config_test.go create mode 100644 receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics.go create mode 100644 receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics_test.go create mode 100644 receiver/k8sclusterreceiver/internal/pod/internal/metadata/testdata/config.yaml create mode 100644 receiver/k8sclusterreceiver/internal/pod/metadata.yaml create mode 100644 receiver/k8sclusterreceiver/internal/pod/testdata/expected.yaml diff --git a/.chloggen/switchk8spod.yaml b/.chloggen/switchk8spod.yaml new file mode 100755 index 000000000000..6ed42002f357 --- /dev/null +++ b/.chloggen/switchk8spod.yaml @@ -0,0 +1,11 @@ +# 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: k8sclusterreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Switch k8s.pod and k8s.container metrics to use pdata. + +# One or more tracking issues related to the change +issues: [23441] diff --git a/receiver/k8sclusterreceiver/e2e_test.go b/receiver/k8sclusterreceiver/e2e_test.go index 94d5cfbc5146..81d22a779cf2 100644 --- a/receiver/k8sclusterreceiver/e2e_test.go +++ b/receiver/k8sclusterreceiver/e2e_test.go @@ -39,6 +39,10 @@ const testKubeConfig = "/tmp/kube-config-otelcol-e2e-testing" // make docker-otelcontribcol // KUBECONFIG=/tmp/kube-config-otelcol-e2e-testing kind load docker-image otelcontribcol:latest func TestE2E(t *testing.T) { + var expected pmetric.Metrics + expectedFile := filepath.Join("testdata", "e2e", "expected.yaml") + expected, err := golden.ReadMetrics(expectedFile) + require.NoError(t, err) kubeConfig, err := clientcmd.BuildConfigFromFlags("", testKubeConfig) require.NoError(t, err) dynamicClient, err := dynamic.NewForConfig(kubeConfig) @@ -57,11 +61,6 @@ func TestE2E(t *testing.T) { wantEntries := 10 // Minimal number of metrics to wait for. waitForData(t, wantEntries, metricsConsumer) - var expected pmetric.Metrics - expectedFile := filepath.Join("testdata", "e2e", "expected.yaml") - expected, err = golden.ReadMetrics(expectedFile) - require.NoError(t, err) - require.NoError(t, err) replaceWithStar := func(string) string { return "*" } shortenNames := func(value string) string { if strings.HasPrefix(value, "kube-proxy") { @@ -82,7 +81,7 @@ func TestE2E(t *testing.T) { return value } containerImageShorten := func(value string) string { - return value[strings.LastIndex(value, "/"):] + return value[(strings.LastIndex(value, "/") + 1):] } require.NoError(t, pmetrictest.CompareMetrics(expected, metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1], pmetrictest.IgnoreTimestamp(), diff --git a/receiver/k8sclusterreceiver/internal/collection/collector.go b/receiver/k8sclusterreceiver/internal/collection/collector.go index 06b916ae1f76..8e9e3b6439ff 100644 --- a/receiver/k8sclusterreceiver/internal/collection/collector.go +++ b/receiver/k8sclusterreceiver/internal/collection/collector.go @@ -104,7 +104,7 @@ func (dc *DataCollector) SyncMetrics(obj interface{}) { switch o := obj.(type) { case *corev1.Pod: - md = ocsToMetrics(pod.GetMetrics(o, dc.settings.TelemetrySettings.Logger)) + md = pod.GetMetrics(dc.settings, o) case *corev1.Node: md = node.GetMetrics(dc.settings, o, dc.nodeConditionsToReport, dc.allocatableTypesToReport) case *corev1.Namespace: diff --git a/receiver/k8sclusterreceiver/internal/container/containers.go b/receiver/k8sclusterreceiver/internal/container/containers.go index 04f0ecabee7f..50d038ad6ca5 100644 --- a/receiver/k8sclusterreceiver/internal/container/containers.go +++ b/receiver/k8sclusterreceiver/internal/container/containers.go @@ -4,18 +4,17 @@ package container // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/container" import ( - "fmt" + "time" - metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" - resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" conventions "go.opentelemetry.io/collector/semconv/v1.6.1" - "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/docker" - "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/maps" metadataPkg "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/experimentalmetricmetadata" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" + imetadata "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/container/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/utils" ) @@ -31,127 +30,67 @@ const ( containerStatusTerminated = "terminated" ) -var containerRestartMetric = &metricspb.MetricDescriptor{ - Name: "k8s.container.restarts", - Description: "How many times the container has restarted in the recent past. " + - "This value is pulled directly from the K8s API and the value can go indefinitely high" + - " and be reset to 0 at any time depending on how your kubelet is configured to prune" + - " dead containers. It is best to not depend too much on the exact value but rather look" + - " at it as either == 0, in which case you can conclude there were no restarts in the recent" + - " past, or > 0, in which case you can conclude there were restarts in the recent past, and" + - " not try and analyze the value beyond that.", - Unit: "1", - Type: metricspb.MetricDescriptor_GAUGE_INT64, -} - -var containerReadyMetric = &metricspb.MetricDescriptor{ - Name: "k8s.container.ready", - Description: "Whether a container has passed its readiness probe (0 for no, 1 for yes)", - Type: metricspb.MetricDescriptor_GAUGE_INT64, -} - -// GetStatusMetrics returns metrics about the status of the container. -func GetStatusMetrics(cs corev1.ContainerStatus) []*metricspb.Metric { - metrics := []*metricspb.Metric{ - { - MetricDescriptor: containerRestartMetric, - Timeseries: []*metricspb.TimeSeries{ - utils.GetInt64TimeSeries(int64(cs.RestartCount)), - }, - }, - { - MetricDescriptor: containerReadyMetric, - Timeseries: []*metricspb.TimeSeries{ - utils.GetInt64TimeSeries(boolToInt64(cs.Ready)), - }, - }, - } - - return metrics -} - -func boolToInt64(b bool) int64 { - if b { - return 1 - } - return 0 -} - // GetSpecMetrics metricizes values from the container spec. // This includes values like resource requests and limits. -func GetSpecMetrics(c corev1.Container) []*metricspb.Metric { - var metrics []*metricspb.Metric - - for _, t := range []struct { - typ string - description string - rl corev1.ResourceList - }{ - { - "request", - "Resource requested for the container. " + - "See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", - c.Resources.Requests, - }, - { - "limit", - "Maximum resource limit set for the container. " + - "See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", - c.Resources.Limits, - }, - } { - for k, v := range t.rl { - val := utils.GetInt64TimeSeries(v.Value()) - valType := metricspb.MetricDescriptor_GAUGE_INT64 - if k == corev1.ResourceCPU { - // cpu metrics must be of the double type to adhere to opentelemetry system.cpu metric specifications - valType = metricspb.MetricDescriptor_GAUGE_DOUBLE - val = utils.GetDoubleTimeSeries(float64(v.MilliValue()) / 1000.0) - } - metrics = append(metrics, - &metricspb.Metric{ - MetricDescriptor: &metricspb.MetricDescriptor{ - Name: fmt.Sprintf("k8s.container.%s_%s", k, t.typ), - Description: t.description, - Type: valType, - }, - Timeseries: []*metricspb.TimeSeries{ - val, - }, - }, - ) +func GetSpecMetrics(set receiver.CreateSettings, c corev1.Container, pod *corev1.Pod) pmetric.Metrics { + mb := imetadata.NewMetricsBuilder(imetadata.DefaultMetricsBuilderConfig(), set) + ts := pcommon.NewTimestampFromTime(time.Now()) + for k, r := range c.Resources.Requests { + switch k { + case corev1.ResourceCPU: + mb.RecordK8sContainerCPURequestDataPoint(ts, float64(r.MilliValue())/1000.0) + case corev1.ResourceMemory: + mb.RecordK8sContainerMemoryRequestDataPoint(ts, r.Value()) + case corev1.ResourceStorage: + mb.RecordK8sContainerStorageRequestDataPoint(ts, r.Value()) + case corev1.ResourceEphemeralStorage: + mb.RecordK8sContainerEphemeralstorageRequestDataPoint(ts, r.Value()) } } - - return metrics -} - -// GetResource returns a proto representation of the pod. -func GetResource(labels map[string]string) *resourcepb.Resource { - return &resourcepb.Resource{ - Type: constants.ContainerType, - Labels: labels, + for k, l := range c.Resources.Limits { + switch k { + case corev1.ResourceCPU: + mb.RecordK8sContainerCPULimitDataPoint(ts, float64(l.MilliValue())/1000.0) + case corev1.ResourceMemory: + mb.RecordK8sContainerMemoryLimitDataPoint(ts, l.Value()) + case corev1.ResourceStorage: + mb.RecordK8sContainerStorageLimitDataPoint(ts, l.Value()) + case corev1.ResourceEphemeralStorage: + mb.RecordK8sContainerEphemeralstorageLimitDataPoint(ts, l.Value()) + } + } + var containerID string + var imageStr string + for _, cs := range pod.Status.ContainerStatuses { + if cs.Name == c.Name { + containerID = cs.ContainerID + imageStr = cs.Image + mb.RecordK8sContainerRestartsDataPoint(ts, int64(cs.RestartCount)) + mb.RecordK8sContainerReadyDataPoint(ts, boolToInt64(cs.Ready)) + break + } } -} - -// GetAllLabels returns all container labels, including ones from -// the pod in which the container is running. -func GetAllLabels(cs corev1.ContainerStatus, - dims map[string]string, logger *zap.Logger) map[string]string { - image, err := docker.ParseImageName(cs.Image) + resourceOptions := []imetadata.ResourceMetricsOption{ + imetadata.WithK8sPodUID(string(pod.UID)), + imetadata.WithK8sPodName(pod.Name), + imetadata.WithK8sNodeName(pod.Spec.NodeName), + imetadata.WithK8sNamespaceName(pod.Namespace), + imetadata.WithOpencensusResourcetype("container"), + imetadata.WithContainerID(utils.StripContainerID(containerID)), + imetadata.WithK8sContainerName(c.Name), + } + image, err := docker.ParseImageName(imageStr) if err != nil { - docker.LogParseError(err, cs.Image, logger) + docker.LogParseError(err, imageStr, set.Logger) + } else { + resourceOptions = append(resourceOptions, + imetadata.WithContainerImageName(image.Repository), + imetadata.WithContainerImageTag(image.Tag)) } - - out := maps.CloneStringMap(dims) - - out[conventions.AttributeContainerID] = utils.StripContainerID(cs.ContainerID) - out[conventions.AttributeK8SContainerName] = cs.Name - out[conventions.AttributeContainerImageName] = image.Repository - out[conventions.AttributeContainerImageTag] = image.Tag - - return out + return mb.Emit( + resourceOptions..., + ) } func GetMetadata(cs corev1.ContainerStatus) *metadata.KubernetesMetadata { @@ -177,3 +116,10 @@ func GetMetadata(cs corev1.ContainerStatus) *metadata.KubernetesMetadata { Metadata: mdata, } } + +func boolToInt64(b bool) int64 { + if b { + return 1 + } + return 0 +} diff --git a/receiver/k8sclusterreceiver/internal/container/doc.go b/receiver/k8sclusterreceiver/internal/container/doc.go new file mode 100644 index 000000000000..c9151ac82319 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +package container // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/container" diff --git a/receiver/k8sclusterreceiver/internal/container/documentation.md b/receiver/k8sclusterreceiver/internal/container/documentation.md new file mode 100644 index 000000000000..e158ff92f3e3 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/documentation.md @@ -0,0 +1,107 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# k8s/container + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### k8s.container.cpu_limit + +Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Double | + +### k8s.container.cpu_request + +Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Double | + +### k8s.container.ephemeralstorage_limit + +Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +### k8s.container.ephemeralstorage_request + +Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +### k8s.container.memory_limit + +Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +### k8s.container.memory_request + +Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +### k8s.container.ready + +Whether a container has passed its readiness probe (0 for no, 1 for yes) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.container.restarts + +How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.container.storage_limit + +Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +### k8s.container.storage_request + +Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| | Gauge | Int | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| container.id | The container id. | Any Str | true | +| container.image.name | The container image name | Any Str | true | +| container.image.tag | The container image tag | Any Str | true | +| k8s.container.name | The k8s container name | Any Str | true | +| k8s.namespace.name | The k8s namespace name | Any Str | true | +| k8s.node.name | The k8s node name | Any Str | true | +| k8s.pod.name | The k8s pod name | Any Str | true | +| k8s.pod.uid | The k8s pod uid | Any Str | true | +| opencensus.resourcetype | The OpenCensus resource type. | Any Str | true | diff --git a/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config.go b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config.go new file mode 100644 index 000000000000..bbd90524abe1 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config.go @@ -0,0 +1,136 @@ +// 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, confmap.WithErrorUnused()) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for k8s/container metrics. +type MetricsConfig struct { + K8sContainerCPULimit MetricConfig `mapstructure:"k8s.container.cpu_limit"` + K8sContainerCPURequest MetricConfig `mapstructure:"k8s.container.cpu_request"` + K8sContainerEphemeralstorageLimit MetricConfig `mapstructure:"k8s.container.ephemeralstorage_limit"` + K8sContainerEphemeralstorageRequest MetricConfig `mapstructure:"k8s.container.ephemeralstorage_request"` + K8sContainerMemoryLimit MetricConfig `mapstructure:"k8s.container.memory_limit"` + K8sContainerMemoryRequest MetricConfig `mapstructure:"k8s.container.memory_request"` + K8sContainerReady MetricConfig `mapstructure:"k8s.container.ready"` + K8sContainerRestarts MetricConfig `mapstructure:"k8s.container.restarts"` + K8sContainerStorageLimit MetricConfig `mapstructure:"k8s.container.storage_limit"` + K8sContainerStorageRequest MetricConfig `mapstructure:"k8s.container.storage_request"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + K8sContainerCPULimit: MetricConfig{ + Enabled: true, + }, + K8sContainerCPURequest: MetricConfig{ + Enabled: true, + }, + K8sContainerEphemeralstorageLimit: MetricConfig{ + Enabled: true, + }, + K8sContainerEphemeralstorageRequest: MetricConfig{ + Enabled: true, + }, + K8sContainerMemoryLimit: MetricConfig{ + Enabled: true, + }, + K8sContainerMemoryRequest: MetricConfig{ + Enabled: true, + }, + K8sContainerReady: MetricConfig{ + Enabled: true, + }, + K8sContainerRestarts: MetricConfig{ + Enabled: true, + }, + K8sContainerStorageLimit: MetricConfig{ + Enabled: true, + }, + K8sContainerStorageRequest: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// ResourceAttributesConfig provides config for k8s/container resource attributes. +type ResourceAttributesConfig struct { + ContainerID ResourceAttributeConfig `mapstructure:"container.id"` + ContainerImageName ResourceAttributeConfig `mapstructure:"container.image.name"` + ContainerImageTag ResourceAttributeConfig `mapstructure:"container.image.tag"` + K8sContainerName ResourceAttributeConfig `mapstructure:"k8s.container.name"` + K8sNamespaceName ResourceAttributeConfig `mapstructure:"k8s.namespace.name"` + K8sNodeName ResourceAttributeConfig `mapstructure:"k8s.node.name"` + K8sPodName ResourceAttributeConfig `mapstructure:"k8s.pod.name"` + K8sPodUID ResourceAttributeConfig `mapstructure:"k8s.pod.uid"` + OpencensusResourcetype ResourceAttributeConfig `mapstructure:"opencensus.resourcetype"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + ContainerID: ResourceAttributeConfig{ + Enabled: true, + }, + ContainerImageName: ResourceAttributeConfig{ + Enabled: true, + }, + ContainerImageTag: ResourceAttributeConfig{ + Enabled: true, + }, + K8sContainerName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sNamespaceName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sNodeName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sPodName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sPodUID: ResourceAttributeConfig{ + Enabled: true, + }, + OpencensusResourcetype: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for k8s/container metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), + } +} diff --git a/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config_test.go b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config_test.go new file mode 100644 index 000000000000..d41e09165290 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config_test.go @@ -0,0 +1,100 @@ +// 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/component" + "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{ + K8sContainerCPULimit: MetricConfig{Enabled: true}, + K8sContainerCPURequest: MetricConfig{Enabled: true}, + K8sContainerEphemeralstorageLimit: MetricConfig{Enabled: true}, + K8sContainerEphemeralstorageRequest: MetricConfig{Enabled: true}, + K8sContainerMemoryLimit: MetricConfig{Enabled: true}, + K8sContainerMemoryRequest: MetricConfig{Enabled: true}, + K8sContainerReady: MetricConfig{Enabled: true}, + K8sContainerRestarts: MetricConfig{Enabled: true}, + K8sContainerStorageLimit: MetricConfig{Enabled: true}, + K8sContainerStorageRequest: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ContainerID: ResourceAttributeConfig{Enabled: true}, + ContainerImageName: ResourceAttributeConfig{Enabled: true}, + ContainerImageTag: ResourceAttributeConfig{Enabled: true}, + K8sContainerName: ResourceAttributeConfig{Enabled: true}, + K8sNamespaceName: ResourceAttributeConfig{Enabled: true}, + K8sNodeName: ResourceAttributeConfig{Enabled: true}, + K8sPodName: ResourceAttributeConfig{Enabled: true}, + K8sPodUID: ResourceAttributeConfig{Enabled: true}, + OpencensusResourcetype: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + K8sContainerCPULimit: MetricConfig{Enabled: false}, + K8sContainerCPURequest: MetricConfig{Enabled: false}, + K8sContainerEphemeralstorageLimit: MetricConfig{Enabled: false}, + K8sContainerEphemeralstorageRequest: MetricConfig{Enabled: false}, + K8sContainerMemoryLimit: MetricConfig{Enabled: false}, + K8sContainerMemoryRequest: MetricConfig{Enabled: false}, + K8sContainerReady: MetricConfig{Enabled: false}, + K8sContainerRestarts: MetricConfig{Enabled: false}, + K8sContainerStorageLimit: MetricConfig{Enabled: false}, + K8sContainerStorageRequest: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ContainerID: ResourceAttributeConfig{Enabled: false}, + ContainerImageName: ResourceAttributeConfig{Enabled: false}, + ContainerImageTag: ResourceAttributeConfig{Enabled: false}, + K8sContainerName: ResourceAttributeConfig{Enabled: false}, + K8sNamespaceName: ResourceAttributeConfig{Enabled: false}, + K8sNodeName: ResourceAttributeConfig{Enabled: false}, + K8sPodName: ResourceAttributeConfig{Enabled: false}, + K8sPodUID: ResourceAttributeConfig{Enabled: false}, + OpencensusResourcetype: ResourceAttributeConfig{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{}, ResourceAttributeConfig{})); 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, component.UnmarshalConfig(sub, &cfg)) + return cfg +} diff --git a/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics.go b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics.go new file mode 100644 index 000000000000..7be82830ee02 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics.go @@ -0,0 +1,773 @@ +// 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.18.0" +) + +type metricK8sContainerCPULimit 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 k8s.container.cpu_limit metric with initial data. +func (m *metricK8sContainerCPULimit) init() { + m.data.SetName("k8s.container.cpu_limit") + m.data.SetDescription("Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerCPULimit) 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 *metricK8sContainerCPULimit) 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 *metricK8sContainerCPULimit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerCPULimit(cfg MetricConfig) metricK8sContainerCPULimit { + m := metricK8sContainerCPULimit{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerCPURequest 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 k8s.container.cpu_request metric with initial data. +func (m *metricK8sContainerCPURequest) init() { + m.data.SetName("k8s.container.cpu_request") + m.data.SetDescription("Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerCPURequest) 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 *metricK8sContainerCPURequest) 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 *metricK8sContainerCPURequest) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerCPURequest(cfg MetricConfig) metricK8sContainerCPURequest { + m := metricK8sContainerCPURequest{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerEphemeralstorageLimit 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 k8s.container.ephemeralstorage_limit metric with initial data. +func (m *metricK8sContainerEphemeralstorageLimit) init() { + m.data.SetName("k8s.container.ephemeralstorage_limit") + m.data.SetDescription("Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerEphemeralstorageLimit) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerEphemeralstorageLimit) 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 *metricK8sContainerEphemeralstorageLimit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerEphemeralstorageLimit(cfg MetricConfig) metricK8sContainerEphemeralstorageLimit { + m := metricK8sContainerEphemeralstorageLimit{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerEphemeralstorageRequest 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 k8s.container.ephemeralstorage_request metric with initial data. +func (m *metricK8sContainerEphemeralstorageRequest) init() { + m.data.SetName("k8s.container.ephemeralstorage_request") + m.data.SetDescription("Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerEphemeralstorageRequest) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerEphemeralstorageRequest) 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 *metricK8sContainerEphemeralstorageRequest) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerEphemeralstorageRequest(cfg MetricConfig) metricK8sContainerEphemeralstorageRequest { + m := metricK8sContainerEphemeralstorageRequest{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerMemoryLimit 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 k8s.container.memory_limit metric with initial data. +func (m *metricK8sContainerMemoryLimit) init() { + m.data.SetName("k8s.container.memory_limit") + m.data.SetDescription("Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerMemoryLimit) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerMemoryLimit) 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 *metricK8sContainerMemoryLimit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerMemoryLimit(cfg MetricConfig) metricK8sContainerMemoryLimit { + m := metricK8sContainerMemoryLimit{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerMemoryRequest 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 k8s.container.memory_request metric with initial data. +func (m *metricK8sContainerMemoryRequest) init() { + m.data.SetName("k8s.container.memory_request") + m.data.SetDescription("Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerMemoryRequest) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerMemoryRequest) 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 *metricK8sContainerMemoryRequest) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerMemoryRequest(cfg MetricConfig) metricK8sContainerMemoryRequest { + m := metricK8sContainerMemoryRequest{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerReady 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 k8s.container.ready metric with initial data. +func (m *metricK8sContainerReady) init() { + m.data.SetName("k8s.container.ready") + m.data.SetDescription("Whether a container has passed its readiness probe (0 for no, 1 for yes)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerReady) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerReady) 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 *metricK8sContainerReady) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerReady(cfg MetricConfig) metricK8sContainerReady { + m := metricK8sContainerReady{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerRestarts 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 k8s.container.restarts metric with initial data. +func (m *metricK8sContainerRestarts) init() { + m.data.SetName("k8s.container.restarts") + m.data.SetDescription("How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that.") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerRestarts) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerRestarts) 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 *metricK8sContainerRestarts) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerRestarts(cfg MetricConfig) metricK8sContainerRestarts { + m := metricK8sContainerRestarts{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerStorageLimit 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 k8s.container.storage_limit metric with initial data. +func (m *metricK8sContainerStorageLimit) init() { + m.data.SetName("k8s.container.storage_limit") + m.data.SetDescription("Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerStorageLimit) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerStorageLimit) 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 *metricK8sContainerStorageLimit) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerStorageLimit(cfg MetricConfig) metricK8sContainerStorageLimit { + m := metricK8sContainerStorageLimit{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sContainerStorageRequest 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 k8s.container.storage_request metric with initial data. +func (m *metricK8sContainerStorageRequest) init() { + m.data.SetName("k8s.container.storage_request") + m.data.SetDescription("Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details") + m.data.SetUnit("") + m.data.SetEmptyGauge() +} + +func (m *metricK8sContainerStorageRequest) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sContainerStorageRequest) 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 *metricK8sContainerStorageRequest) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sContainerStorageRequest(cfg MetricConfig) metricK8sContainerStorageRequest { + m := metricK8sContainerStorageRequest{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 { + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + resourceCapacity int // maximum observed number of resource attributes. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information + resourceAttributesConfig ResourceAttributesConfig + metricK8sContainerCPULimit metricK8sContainerCPULimit + metricK8sContainerCPURequest metricK8sContainerCPURequest + metricK8sContainerEphemeralstorageLimit metricK8sContainerEphemeralstorageLimit + metricK8sContainerEphemeralstorageRequest metricK8sContainerEphemeralstorageRequest + metricK8sContainerMemoryLimit metricK8sContainerMemoryLimit + metricK8sContainerMemoryRequest metricK8sContainerMemoryRequest + metricK8sContainerReady metricK8sContainerReady + metricK8sContainerRestarts metricK8sContainerRestarts + metricK8sContainerStorageLimit metricK8sContainerStorageLimit + metricK8sContainerStorageRequest metricK8sContainerStorageRequest +} + +// metricBuilderOption applies changes to default metrics builder. +type metricBuilderOption func(*MetricsBuilder) + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) metricBuilderOption { + return func(mb *MetricsBuilder) { + mb.startTime = startTime + } +} + +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSettings, options ...metricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + resourceAttributesConfig: mbc.ResourceAttributes, + metricK8sContainerCPULimit: newMetricK8sContainerCPULimit(mbc.Metrics.K8sContainerCPULimit), + metricK8sContainerCPURequest: newMetricK8sContainerCPURequest(mbc.Metrics.K8sContainerCPURequest), + metricK8sContainerEphemeralstorageLimit: newMetricK8sContainerEphemeralstorageLimit(mbc.Metrics.K8sContainerEphemeralstorageLimit), + metricK8sContainerEphemeralstorageRequest: newMetricK8sContainerEphemeralstorageRequest(mbc.Metrics.K8sContainerEphemeralstorageRequest), + metricK8sContainerMemoryLimit: newMetricK8sContainerMemoryLimit(mbc.Metrics.K8sContainerMemoryLimit), + metricK8sContainerMemoryRequest: newMetricK8sContainerMemoryRequest(mbc.Metrics.K8sContainerMemoryRequest), + metricK8sContainerReady: newMetricK8sContainerReady(mbc.Metrics.K8sContainerReady), + metricK8sContainerRestarts: newMetricK8sContainerRestarts(mbc.Metrics.K8sContainerRestarts), + metricK8sContainerStorageLimit: newMetricK8sContainerStorageLimit(mbc.Metrics.K8sContainerStorageLimit), + metricK8sContainerStorageRequest: newMetricK8sContainerStorageRequest(mbc.Metrics.K8sContainerStorageRequest), + } + for _, op := range options { + op(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() + } + if mb.resourceCapacity < rm.Resource().Attributes().Len() { + mb.resourceCapacity = rm.Resource().Attributes().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption func(ResourceAttributesConfig, pmetric.ResourceMetrics) + +// WithContainerID sets provided value as "container.id" attribute for current resource. +func WithContainerID(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.ContainerID.Enabled { + rm.Resource().Attributes().PutStr("container.id", val) + } + } +} + +// WithContainerImageName sets provided value as "container.image.name" attribute for current resource. +func WithContainerImageName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.ContainerImageName.Enabled { + rm.Resource().Attributes().PutStr("container.image.name", val) + } + } +} + +// WithContainerImageTag sets provided value as "container.image.tag" attribute for current resource. +func WithContainerImageTag(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.ContainerImageTag.Enabled { + rm.Resource().Attributes().PutStr("container.image.tag", val) + } + } +} + +// WithK8sContainerName sets provided value as "k8s.container.name" attribute for current resource. +func WithK8sContainerName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sContainerName.Enabled { + rm.Resource().Attributes().PutStr("k8s.container.name", val) + } + } +} + +// WithK8sNamespaceName sets provided value as "k8s.namespace.name" attribute for current resource. +func WithK8sNamespaceName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNamespaceName.Enabled { + rm.Resource().Attributes().PutStr("k8s.namespace.name", val) + } + } +} + +// WithK8sNodeName sets provided value as "k8s.node.name" attribute for current resource. +func WithK8sNodeName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNodeName.Enabled { + rm.Resource().Attributes().PutStr("k8s.node.name", val) + } + } +} + +// WithK8sPodName sets provided value as "k8s.pod.name" attribute for current resource. +func WithK8sPodName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sPodName.Enabled { + rm.Resource().Attributes().PutStr("k8s.pod.name", val) + } + } +} + +// WithK8sPodUID sets provided value as "k8s.pod.uid" attribute for current resource. +func WithK8sPodUID(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sPodUID.Enabled { + rm.Resource().Attributes().PutStr("k8s.pod.uid", val) + } + } +} + +// WithOpencensusResourcetype sets provided value as "opencensus.resourcetype" attribute for current resource. +func WithOpencensusResourcetype(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.OpencensusResourcetype.Enabled { + rm.Resource().Attributes().PutStr("opencensus.resourcetype", val) + } + } +} + +// 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 func(_ ResourceAttributesConfig, 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(rmo ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + rm.SetSchemaUrl(conventions.SchemaURL) + rm.Resource().Attributes().EnsureCapacity(mb.resourceCapacity) + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName("otelcol/k8sclusterreceiver") + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricK8sContainerCPULimit.emit(ils.Metrics()) + mb.metricK8sContainerCPURequest.emit(ils.Metrics()) + mb.metricK8sContainerEphemeralstorageLimit.emit(ils.Metrics()) + mb.metricK8sContainerEphemeralstorageRequest.emit(ils.Metrics()) + mb.metricK8sContainerMemoryLimit.emit(ils.Metrics()) + mb.metricK8sContainerMemoryRequest.emit(ils.Metrics()) + mb.metricK8sContainerReady.emit(ils.Metrics()) + mb.metricK8sContainerRestarts.emit(ils.Metrics()) + mb.metricK8sContainerStorageLimit.emit(ils.Metrics()) + mb.metricK8sContainerStorageRequest.emit(ils.Metrics()) + + for _, op := range rmo { + op(mb.resourceAttributesConfig, 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(rmo ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(rmo...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordK8sContainerCPULimitDataPoint adds a data point to k8s.container.cpu_limit metric. +func (mb *MetricsBuilder) RecordK8sContainerCPULimitDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricK8sContainerCPULimit.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerCPURequestDataPoint adds a data point to k8s.container.cpu_request metric. +func (mb *MetricsBuilder) RecordK8sContainerCPURequestDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricK8sContainerCPURequest.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerEphemeralstorageLimitDataPoint adds a data point to k8s.container.ephemeralstorage_limit metric. +func (mb *MetricsBuilder) RecordK8sContainerEphemeralstorageLimitDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerEphemeralstorageLimit.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerEphemeralstorageRequestDataPoint adds a data point to k8s.container.ephemeralstorage_request metric. +func (mb *MetricsBuilder) RecordK8sContainerEphemeralstorageRequestDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerEphemeralstorageRequest.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerMemoryLimitDataPoint adds a data point to k8s.container.memory_limit metric. +func (mb *MetricsBuilder) RecordK8sContainerMemoryLimitDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerMemoryLimit.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerMemoryRequestDataPoint adds a data point to k8s.container.memory_request metric. +func (mb *MetricsBuilder) RecordK8sContainerMemoryRequestDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerMemoryRequest.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerReadyDataPoint adds a data point to k8s.container.ready metric. +func (mb *MetricsBuilder) RecordK8sContainerReadyDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerReady.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerRestartsDataPoint adds a data point to k8s.container.restarts metric. +func (mb *MetricsBuilder) RecordK8sContainerRestartsDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerRestarts.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerStorageLimitDataPoint adds a data point to k8s.container.storage_limit metric. +func (mb *MetricsBuilder) RecordK8sContainerStorageLimitDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerStorageLimit.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sContainerStorageRequestDataPoint adds a data point to k8s.container.storage_request metric. +func (mb *MetricsBuilder) RecordK8sContainerStorageRequestDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sContainerStorageRequest.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(mb) + } +} diff --git a/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics_test.go b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics_test.go new file mode 100644 index 000000000000..1112031c81d8 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics_test.go @@ -0,0 +1,309 @@ +// 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 testConfigCollection int + +const ( + testSetDefault testConfigCollection = iota + testSetAll + testSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + configSet testConfigCollection + }{ + { + name: "default", + configSet: testSetDefault, + }, + { + name: "all_set", + configSet: testSetAll, + }, + { + name: "none_set", + configSet: testSetNone, + }, + } + for _, test := range tests { + t.Run(test.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.NewNopCreateSettings() + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, test.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerCPULimitDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerCPURequestDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerEphemeralstorageLimitDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerEphemeralstorageRequestDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerMemoryLimitDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerMemoryRequestDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerReadyDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerRestartsDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerStorageLimitDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerStorageRequestDataPoint(ts, 1) + + metrics := mb.Emit(WithContainerID("attr-val"), WithContainerImageName("attr-val"), WithContainerImageTag("attr-val"), WithK8sContainerName("attr-val"), WithK8sNamespaceName("attr-val"), WithK8sNodeName("attr-val"), WithK8sPodName("attr-val"), WithK8sPodUID("attr-val"), WithOpencensusResourcetype("attr-val")) + + if test.configSet == testSetNone { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + attrCount := 0 + enabledAttrCount := 0 + attrVal, ok := rm.Resource().Attributes().Get("container.id") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.ContainerID.Enabled, ok) + if mb.resourceAttributesConfig.ContainerID.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("container.image.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.ContainerImageName.Enabled, ok) + if mb.resourceAttributesConfig.ContainerImageName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("container.image.tag") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.ContainerImageTag.Enabled, ok) + if mb.resourceAttributesConfig.ContainerImageTag.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.container.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sContainerName.Enabled, ok) + if mb.resourceAttributesConfig.K8sContainerName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.namespace.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNamespaceName.Enabled, ok) + if mb.resourceAttributesConfig.K8sNamespaceName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.node.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNodeName.Enabled, ok) + if mb.resourceAttributesConfig.K8sNodeName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.pod.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sPodName.Enabled, ok) + if mb.resourceAttributesConfig.K8sPodName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.pod.uid") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sPodUID.Enabled, ok) + if mb.resourceAttributesConfig.K8sPodUID.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("opencensus.resourcetype") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.OpencensusResourcetype.Enabled, ok) + if mb.resourceAttributesConfig.OpencensusResourcetype.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + assert.Equal(t, enabledAttrCount, rm.Resource().Attributes().Len()) + assert.Equal(t, attrCount, 9) + + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if test.configSet == testSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if test.configSet == testSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "k8s.container.cpu_limit": + assert.False(t, validatedMetrics["k8s.container.cpu_limit"], "Found a duplicate in the metrics slice: k8s.container.cpu_limit") + validatedMetrics["k8s.container.cpu_limit"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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()) + case "k8s.container.cpu_request": + assert.False(t, validatedMetrics["k8s.container.cpu_request"], "Found a duplicate in the metrics slice: k8s.container.cpu_request") + validatedMetrics["k8s.container.cpu_request"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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()) + case "k8s.container.ephemeralstorage_limit": + assert.False(t, validatedMetrics["k8s.container.ephemeralstorage_limit"], "Found a duplicate in the metrics slice: k8s.container.ephemeralstorage_limit") + validatedMetrics["k8s.container.ephemeralstorage_limit"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.ephemeralstorage_request": + assert.False(t, validatedMetrics["k8s.container.ephemeralstorage_request"], "Found a duplicate in the metrics slice: k8s.container.ephemeralstorage_request") + validatedMetrics["k8s.container.ephemeralstorage_request"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.memory_limit": + assert.False(t, validatedMetrics["k8s.container.memory_limit"], "Found a duplicate in the metrics slice: k8s.container.memory_limit") + validatedMetrics["k8s.container.memory_limit"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.memory_request": + assert.False(t, validatedMetrics["k8s.container.memory_request"], "Found a duplicate in the metrics slice: k8s.container.memory_request") + validatedMetrics["k8s.container.memory_request"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.ready": + assert.False(t, validatedMetrics["k8s.container.ready"], "Found a duplicate in the metrics slice: k8s.container.ready") + validatedMetrics["k8s.container.ready"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Whether a container has passed its readiness probe (0 for no, 1 for yes)", ms.At(i).Description()) + assert.Equal(t, "1", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.restarts": + assert.False(t, validatedMetrics["k8s.container.restarts"], "Found a duplicate in the metrics slice: k8s.container.restarts") + validatedMetrics["k8s.container.restarts"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that.", ms.At(i).Description()) + assert.Equal(t, "1", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.storage_limit": + assert.False(t, validatedMetrics["k8s.container.storage_limit"], "Found a duplicate in the metrics slice: k8s.container.storage_limit") + validatedMetrics["k8s.container.storage_limit"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "k8s.container.storage_request": + assert.False(t, validatedMetrics["k8s.container.storage_request"], "Found a duplicate in the metrics slice: k8s.container.storage_request") + validatedMetrics["k8s.container.storage_request"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details", ms.At(i).Description()) + assert.Equal(t, "", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + } + } + }) + } +} diff --git a/receiver/k8sclusterreceiver/internal/container/internal/metadata/testdata/config.yaml b/receiver/k8sclusterreceiver/internal/container/internal/metadata/testdata/config.yaml new file mode 100644 index 000000000000..f8c130aadc10 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/testdata/config.yaml @@ -0,0 +1,83 @@ +default: +all_set: + metrics: + k8s.container.cpu_limit: + enabled: true + k8s.container.cpu_request: + enabled: true + k8s.container.ephemeralstorage_limit: + enabled: true + k8s.container.ephemeralstorage_request: + enabled: true + k8s.container.memory_limit: + enabled: true + k8s.container.memory_request: + enabled: true + k8s.container.ready: + enabled: true + k8s.container.restarts: + enabled: true + k8s.container.storage_limit: + enabled: true + k8s.container.storage_request: + enabled: true + resource_attributes: + container.id: + enabled: true + container.image.name: + enabled: true + container.image.tag: + enabled: true + k8s.container.name: + enabled: true + k8s.namespace.name: + enabled: true + k8s.node.name: + enabled: true + k8s.pod.name: + enabled: true + k8s.pod.uid: + enabled: true + opencensus.resourcetype: + enabled: true +none_set: + metrics: + k8s.container.cpu_limit: + enabled: false + k8s.container.cpu_request: + enabled: false + k8s.container.ephemeralstorage_limit: + enabled: false + k8s.container.ephemeralstorage_request: + enabled: false + k8s.container.memory_limit: + enabled: false + k8s.container.memory_request: + enabled: false + k8s.container.ready: + enabled: false + k8s.container.restarts: + enabled: false + k8s.container.storage_limit: + enabled: false + k8s.container.storage_request: + enabled: false + resource_attributes: + container.id: + enabled: false + container.image.name: + enabled: false + container.image.tag: + enabled: false + k8s.container.name: + enabled: false + k8s.namespace.name: + enabled: false + k8s.node.name: + enabled: false + k8s.pod.name: + enabled: false + k8s.pod.uid: + enabled: false + opencensus.resourcetype: + enabled: false diff --git a/receiver/k8sclusterreceiver/internal/container/metadata.yaml b/receiver/k8sclusterreceiver/internal/container/metadata.yaml new file mode 100644 index 000000000000..c818e51b63a2 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/metadata.yaml @@ -0,0 +1,103 @@ +type: k8s/container + +sem_conv_version: 1.18.0 + +resource_attributes: + container.id: + description: The container id. + type: string + enabled: true + + container.image.name: + description: The container image name + type: string + enabled: true + + container.image.tag: + description: The container image tag + type: string + enabled: true + + k8s.container.name: + description: The k8s container name + type: string + enabled: true + + k8s.namespace.name: + description: The k8s namespace name + type: string + enabled: true + + k8s.node.name: + description: The k8s node name + type: string + enabled: true + + k8s.pod.name: + description: The k8s pod name + type: string + enabled: true + + k8s.pod.uid: + description: The k8s pod uid + type: string + enabled: true + + opencensus.resourcetype: + description: The OpenCensus resource type. + type: string + enabled: true + +metrics: + k8s.container.cpu_request: + enabled: true + description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: double + k8s.container.cpu_limit: + enabled: true + description: Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: double + k8s.container.memory_request: + enabled: true + description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.memory_limit: + enabled: true + description: Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.storage_request: + enabled: true + description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.storage_limit: + enabled: true + description: Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.ephemeralstorage_request: + enabled: true + description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.ephemeralstorage_limit: + enabled: true + description: Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + value_type: int + k8s.container.restarts: + enabled: true + description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. + unit: 1 + gauge: + value_type: int + k8s.container.ready: + enabled: true + description: Whether a container has passed its readiness probe (0 for no, 1 for yes) + unit: 1 + gauge: + value_type: int \ No newline at end of file diff --git a/receiver/k8sclusterreceiver/internal/pod/doc.go b/receiver/k8sclusterreceiver/internal/pod/doc.go new file mode 100644 index 000000000000..959c8bf5e5f0 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +package pod // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/pod" diff --git a/receiver/k8sclusterreceiver/internal/pod/documentation.md b/receiver/k8sclusterreceiver/internal/pod/documentation.md new file mode 100644 index 000000000000..36b16d19bfc2 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/documentation.md @@ -0,0 +1,31 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# k8s/pod + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### k8s.pod.phase + +Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| k8s.namespace.name | The k8s namespace name. | Any Str | true | +| k8s.node.name | The k8s node name. | Any Str | true | +| k8s.pod.name | The k8s pod name. | Any Str | true | +| k8s.pod.uid | The k8s pod uid. | Any Str | true | +| opencensus.resourcetype | The OpenCensus resource type. | Any Str | true | diff --git a/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config.go b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config.go new file mode 100644 index 000000000000..28ec4922e5be --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config.go @@ -0,0 +1,84 @@ +// 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, confmap.WithErrorUnused()) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for k8s/pod metrics. +type MetricsConfig struct { + K8sPodPhase MetricConfig `mapstructure:"k8s.pod.phase"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + K8sPodPhase: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// ResourceAttributesConfig provides config for k8s/pod resource attributes. +type ResourceAttributesConfig struct { + K8sNamespaceName ResourceAttributeConfig `mapstructure:"k8s.namespace.name"` + K8sNodeName ResourceAttributeConfig `mapstructure:"k8s.node.name"` + K8sPodName ResourceAttributeConfig `mapstructure:"k8s.pod.name"` + K8sPodUID ResourceAttributeConfig `mapstructure:"k8s.pod.uid"` + OpencensusResourcetype ResourceAttributeConfig `mapstructure:"opencensus.resourcetype"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sNodeName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sPodName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sPodUID: ResourceAttributeConfig{ + Enabled: true, + }, + OpencensusResourcetype: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for k8s/pod metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), + } +} diff --git a/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config_test.go b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config_test.go new file mode 100644 index 000000000000..e8ebeec5b9f8 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_config_test.go @@ -0,0 +1,74 @@ +// 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/component" + "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{ + K8sPodPhase: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{Enabled: true}, + K8sNodeName: ResourceAttributeConfig{Enabled: true}, + K8sPodName: ResourceAttributeConfig{Enabled: true}, + K8sPodUID: ResourceAttributeConfig{Enabled: true}, + OpencensusResourcetype: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + K8sPodPhase: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{Enabled: false}, + K8sNodeName: ResourceAttributeConfig{Enabled: false}, + K8sPodName: ResourceAttributeConfig{Enabled: false}, + K8sPodUID: ResourceAttributeConfig{Enabled: false}, + OpencensusResourcetype: ResourceAttributeConfig{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{}, ResourceAttributeConfig{})); 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, component.UnmarshalConfig(sub, &cfg)) + return cfg +} diff --git a/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics.go b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics.go new file mode 100644 index 000000000000..28137ef4183f --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics.go @@ -0,0 +1,224 @@ +// 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.18.0" +) + +type metricK8sPodPhase 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 k8s.pod.phase metric with initial data. +func (m *metricK8sPodPhase) init() { + m.data.SetName("k8s.pod.phase") + m.data.SetDescription("Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sPodPhase) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricK8sPodPhase) 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 *metricK8sPodPhase) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sPodPhase(cfg MetricConfig) metricK8sPodPhase { + m := metricK8sPodPhase{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 { + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + resourceCapacity int // maximum observed number of resource attributes. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information + resourceAttributesConfig ResourceAttributesConfig + metricK8sPodPhase metricK8sPodPhase +} + +// metricBuilderOption applies changes to default metrics builder. +type metricBuilderOption func(*MetricsBuilder) + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) metricBuilderOption { + return func(mb *MetricsBuilder) { + mb.startTime = startTime + } +} + +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.CreateSettings, options ...metricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + resourceAttributesConfig: mbc.ResourceAttributes, + metricK8sPodPhase: newMetricK8sPodPhase(mbc.Metrics.K8sPodPhase), + } + for _, op := range options { + op(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() + } + if mb.resourceCapacity < rm.Resource().Attributes().Len() { + mb.resourceCapacity = rm.Resource().Attributes().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption func(ResourceAttributesConfig, pmetric.ResourceMetrics) + +// WithK8sNamespaceName sets provided value as "k8s.namespace.name" attribute for current resource. +func WithK8sNamespaceName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNamespaceName.Enabled { + rm.Resource().Attributes().PutStr("k8s.namespace.name", val) + } + } +} + +// WithK8sNodeName sets provided value as "k8s.node.name" attribute for current resource. +func WithK8sNodeName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNodeName.Enabled { + rm.Resource().Attributes().PutStr("k8s.node.name", val) + } + } +} + +// WithK8sPodName sets provided value as "k8s.pod.name" attribute for current resource. +func WithK8sPodName(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sPodName.Enabled { + rm.Resource().Attributes().PutStr("k8s.pod.name", val) + } + } +} + +// WithK8sPodUID sets provided value as "k8s.pod.uid" attribute for current resource. +func WithK8sPodUID(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sPodUID.Enabled { + rm.Resource().Attributes().PutStr("k8s.pod.uid", val) + } + } +} + +// WithOpencensusResourcetype sets provided value as "opencensus.resourcetype" attribute for current resource. +func WithOpencensusResourcetype(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.OpencensusResourcetype.Enabled { + rm.Resource().Attributes().PutStr("opencensus.resourcetype", val) + } + } +} + +// 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 func(_ ResourceAttributesConfig, 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(rmo ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + rm.SetSchemaUrl(conventions.SchemaURL) + rm.Resource().Attributes().EnsureCapacity(mb.resourceCapacity) + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName("otelcol/k8sclusterreceiver") + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricK8sPodPhase.emit(ils.Metrics()) + + for _, op := range rmo { + op(mb.resourceAttributesConfig, 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(rmo ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(rmo...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordK8sPodPhaseDataPoint adds a data point to k8s.pod.phase metric. +func (mb *MetricsBuilder) RecordK8sPodPhaseDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sPodPhase.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(mb) + } +} diff --git a/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics_test.go b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics_test.go new file mode 100644 index 000000000000..c664d75d9192 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/generated_metrics_test.go @@ -0,0 +1,137 @@ +// 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 testConfigCollection int + +const ( + testSetDefault testConfigCollection = iota + testSetAll + testSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + configSet testConfigCollection + }{ + { + name: "default", + configSet: testSetDefault, + }, + { + name: "all_set", + configSet: testSetAll, + }, + { + name: "none_set", + configSet: testSetNone, + }, + } + for _, test := range tests { + t.Run(test.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.NewNopCreateSettings() + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, test.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sPodPhaseDataPoint(ts, 1) + + metrics := mb.Emit(WithK8sNamespaceName("attr-val"), WithK8sNodeName("attr-val"), WithK8sPodName("attr-val"), WithK8sPodUID("attr-val"), WithOpencensusResourcetype("attr-val")) + + if test.configSet == testSetNone { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + attrCount := 0 + enabledAttrCount := 0 + attrVal, ok := rm.Resource().Attributes().Get("k8s.namespace.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNamespaceName.Enabled, ok) + if mb.resourceAttributesConfig.K8sNamespaceName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.node.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNodeName.Enabled, ok) + if mb.resourceAttributesConfig.K8sNodeName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.pod.name") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sPodName.Enabled, ok) + if mb.resourceAttributesConfig.K8sPodName.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("k8s.pod.uid") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sPodUID.Enabled, ok) + if mb.resourceAttributesConfig.K8sPodUID.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + attrVal, ok = rm.Resource().Attributes().Get("opencensus.resourcetype") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.OpencensusResourcetype.Enabled, ok) + if mb.resourceAttributesConfig.OpencensusResourcetype.Enabled { + enabledAttrCount++ + assert.EqualValues(t, "attr-val", attrVal.Str()) + } + assert.Equal(t, enabledAttrCount, rm.Resource().Attributes().Len()) + assert.Equal(t, attrCount, 5) + + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if test.configSet == testSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if test.configSet == testSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "k8s.pod.phase": + assert.False(t, validatedMetrics["k8s.pod.phase"], "Found a duplicate in the metrics slice: k8s.pod.phase") + validatedMetrics["k8s.pod.phase"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown)", ms.At(i).Description()) + assert.Equal(t, "1", 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.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + } + } + }) + } +} diff --git a/receiver/k8sclusterreceiver/internal/pod/internal/metadata/testdata/config.yaml b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/testdata/config.yaml new file mode 100644 index 000000000000..fe14a2b630a6 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/internal/metadata/testdata/config.yaml @@ -0,0 +1,31 @@ +default: +all_set: + metrics: + k8s.pod.phase: + enabled: true + resource_attributes: + k8s.namespace.name: + enabled: true + k8s.node.name: + enabled: true + k8s.pod.name: + enabled: true + k8s.pod.uid: + enabled: true + opencensus.resourcetype: + enabled: true +none_set: + metrics: + k8s.pod.phase: + enabled: false + resource_attributes: + k8s.namespace.name: + enabled: false + k8s.node.name: + enabled: false + k8s.pod.name: + enabled: false + k8s.pod.uid: + enabled: false + opencensus.resourcetype: + enabled: false diff --git a/receiver/k8sclusterreceiver/internal/pod/metadata.yaml b/receiver/k8sclusterreceiver/internal/pod/metadata.yaml new file mode 100644 index 000000000000..5de46d6246a7 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/metadata.yaml @@ -0,0 +1,37 @@ +type: k8s/pod + +sem_conv_version: 1.18.0 + +resource_attributes: + k8s.namespace.name: + description: The k8s namespace name. + type: string + enabled: true + + k8s.node.name: + description: The k8s node name. + type: string + enabled: true + + k8s.pod.name: + description: The k8s pod name. + type: string + enabled: true + + k8s.pod.uid: + description: The k8s pod uid. + type: string + enabled: true + + opencensus.resourcetype: + description: The OpenCensus resource type. + type: string + enabled: true + +metrics: + k8s.pod.phase: + enabled: true + description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) + unit: 1 + gauge: + value_type: int \ No newline at end of file diff --git a/receiver/k8sclusterreceiver/internal/pod/pods.go b/receiver/k8sclusterreceiver/internal/pod/pods.go index 5801a0f29bb3..75fcd4a0f2b7 100644 --- a/receiver/k8sclusterreceiver/internal/pod/pods.go +++ b/receiver/k8sclusterreceiver/internal/pod/pods.go @@ -4,18 +4,20 @@ package pod // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/pod" import ( + "fmt" "strings" "time" - agentmetricspb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/metrics/v1" - metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" - resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" conventions "go.opentelemetry.io/collector/semconv/v1.6.1" "go.uber.org/zap" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/cache" @@ -24,7 +26,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/container" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/metadata" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/service" + imetadataphase "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/pod/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/utils" ) @@ -33,12 +35,6 @@ const ( podCreationTime = "pod.creation_timestamp" ) -var podPhaseMetric = &metricspb.MetricDescriptor{ - Name: "k8s.pod.phase", - Description: "Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown)", - Type: metricspb.MetricDescriptor_GAUGE_INT64, -} - // Transform transforms the pod to remove the fields that we don't use to reduce RAM utilization. // IMPORTANT: Make sure to update this function before using new pod fields. func Transform(pod *corev1.Pod) *corev1.Pod { @@ -75,75 +71,18 @@ func Transform(pod *corev1.Pod) *corev1.Pod { return newPod } -func GetMetrics(pod *corev1.Pod, logger *zap.Logger) []*agentmetricspb.ExportMetricsServiceRequest { - metrics := []*metricspb.Metric{ - { - MetricDescriptor: podPhaseMetric, - Timeseries: []*metricspb.TimeSeries{ - utils.GetInt64TimeSeries(int64(phaseToInt(pod.Status.Phase))), - }, - }, - } - - podRes := getResource(pod) - - containerResByName := map[string]*agentmetricspb.ExportMetricsServiceRequest{} - - for _, cs := range pod.Status.ContainerStatuses { - contLabels := container.GetAllLabels(cs, podRes.Labels, logger) - containerResByName[cs.Name] = &agentmetricspb.ExportMetricsServiceRequest{Resource: container.GetResource(contLabels)} - - containerResByName[cs.Name].Metrics = container.GetStatusMetrics(cs) - } +func GetMetrics(set receiver.CreateSettings, pod *corev1.Pod) pmetric.Metrics { + mbphase := imetadataphase.NewMetricsBuilder(imetadataphase.DefaultMetricsBuilderConfig(), set) + ts := pcommon.NewTimestampFromTime(time.Now()) + mbphase.RecordK8sPodPhaseDataPoint(ts, int64(phaseToInt(pod.Status.Phase))) + metrics := mbphase.Emit(imetadataphase.WithK8sNamespaceName(pod.Namespace), imetadataphase.WithK8sNodeName(pod.Spec.NodeName), imetadataphase.WithK8sPodName(pod.Name), imetadataphase.WithK8sPodUID(string(pod.UID)), imetadataphase.WithOpencensusResourcetype("k8s")) for _, c := range pod.Spec.Containers { - cr := containerResByName[c.Name] - - // This likely will not happen since both pod spec and status return - // information about the same set of containers. However, if there's - // a mismatch, skip collecting spec metrics. - if cr == nil { - continue - } - - cr.Metrics = append(cr.Metrics, container.GetSpecMetrics(c)...) - } - - out := []*agentmetricspb.ExportMetricsServiceRequest{ - { - Resource: podRes, - Metrics: metrics, - }, + specMetrics := container.GetSpecMetrics(set, c, pod) + specMetrics.ResourceMetrics().MoveAndAppendTo(metrics.ResourceMetrics()) } - out = append(out, listResourceMetrics(containerResByName)...) - - return out -} - -func listResourceMetrics(rms map[string]*agentmetricspb.ExportMetricsServiceRequest) []*agentmetricspb.ExportMetricsServiceRequest { - out := make([]*agentmetricspb.ExportMetricsServiceRequest, len(rms)) - - i := 0 - for _, rm := range rms { - out[i] = rm - i++ - } - - return out -} - -// getResource returns a proto representation of the pod. -func getResource(pod *corev1.Pod) *resourcepb.Resource { - return &resourcepb.Resource{ - Type: constants.K8sType, - Labels: map[string]string{ - conventions.AttributeK8SPodUID: string(pod.UID), - conventions.AttributeK8SPodName: pod.Name, - conventions.AttributeK8SNodeName: pod.Spec.NodeName, - conventions.AttributeK8SNamespaceName: pod.Namespace, - }, - } + return metrics } func phaseToInt(phase corev1.PodPhase) int32 { @@ -183,7 +122,7 @@ func GetMetadata(pod *corev1.Pod, mc *metadata.Store, logger *zap.Logger) map[ex } if mc.Services != nil { - meta = maps.MergeStringMaps(meta, service.GetPodServiceTags(pod, mc.Services)) + meta = maps.MergeStringMaps(meta, getPodServiceTags(pod, mc.Services)) } if mc.Jobs != nil { @@ -267,6 +206,21 @@ func logError(err error, ref *v1.OwnerReference, podUID types.UID, logger *zap.L ) } +// getPodServiceTags returns a set of services associated with the pod. +func getPodServiceTags(pod *corev1.Pod, services cache.Store) map[string]string { + properties := map[string]string{} + + for _, ser := range services.List() { + serObj := ser.(*corev1.Service) + if serObj.Namespace == pod.Namespace && + labels.Set(serObj.Spec.Selector).AsSelectorPreValidated().Matches(labels.Set(pod.Labels)) { + properties[fmt.Sprintf("%s%s", constants.K8sServicePrefix, serObj.Name)] = "" + } + } + + return properties +} + // getWorkloadProperties returns workload metadata for provided owner reference. func getWorkloadProperties(ref *v1.OwnerReference, labelKey string) map[string]string { uidKey := metadata.GetOTelUIDFromKind(strings.ToLower(ref.Kind)) diff --git a/receiver/k8sclusterreceiver/internal/pod/pods_test.go b/receiver/k8sclusterreceiver/internal/pod/pods_test.go index 4985bc86ae0b..ac9d2bfc8caf 100644 --- a/receiver/k8sclusterreceiver/internal/pod/pods_test.go +++ b/receiver/k8sclusterreceiver/internal/pod/pods_test.go @@ -5,15 +5,14 @@ package pod import ( "fmt" + "path/filepath" "strings" "testing" "time" - agentmetricspb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/metrics/v1" - metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" - resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/receiver/receivertest" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" @@ -22,8 +21,9 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/golden" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/experimentalmetricmetadata" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/testutils" ) @@ -41,70 +41,23 @@ func TestPodAndContainerMetricsReportCPUMetrics(t *testing.T) { testutils.NewPodStatusWithContainer("container-name", containerIDWithPreifx("container-id")), ) - actualResourceMetrics := GetMetrics(pod, zap.NewNop()) - - require.Len(t, actualResourceMetrics, 2) - testutils.AssertResource(t, actualResourceMetrics[0].Resource, constants.K8sType, - map[string]string{ - "k8s.pod.uid": "test-pod-1-uid", - "k8s.pod.name": "test-pod-1", - "k8s.node.name": "test-node", - "k8s.namespace.name": "test-namespace", - }, - ) - - require.Len(t, actualResourceMetrics[0].Metrics, 1) - testutils.AssertMetricsInt(t, actualResourceMetrics[0].Metrics[0], "k8s.pod.phase", - metricspb.MetricDescriptor_GAUGE_INT64, 3) - - require.Len(t, actualResourceMetrics[1].Metrics, 4) - testutils.AssertResource(t, actualResourceMetrics[1].Resource, "container", - map[string]string{ - "container.id": "container-id", - "k8s.container.name": "container-name", - "container.image.name": "container-image-name", - "container.image.tag": "latest", - "k8s.pod.uid": "test-pod-1-uid", - "k8s.pod.name": "test-pod-1", - "k8s.node.name": "test-node", - "k8s.namespace.name": "test-namespace", - }, + m := GetMetrics(receivertest.NewNopCreateSettings(), pod) + expected, err := golden.ReadMetrics(filepath.Join("testdata", "expected.yaml")) + require.NoError(t, err) + require.NoError(t, pmetrictest.CompareMetrics(expected, m, + pmetrictest.IgnoreTimestamp(), + pmetrictest.IgnoreStartTimestamp(), + pmetrictest.IgnoreResourceMetricsOrder(), + pmetrictest.IgnoreMetricsOrder(), + pmetrictest.IgnoreScopeMetricsOrder(), + ), ) - - testutils.AssertMetricsInt(t, actualResourceMetrics[1].Metrics[0], "k8s.container.restarts", - metricspb.MetricDescriptor_GAUGE_INT64, 3) - - testutils.AssertMetricsInt(t, actualResourceMetrics[1].Metrics[1], "k8s.container.ready", - metricspb.MetricDescriptor_GAUGE_INT64, 1) - - testutils.AssertMetricsDouble(t, actualResourceMetrics[1].Metrics[2], "k8s.container.cpu_request", - metricspb.MetricDescriptor_GAUGE_DOUBLE, 10.0) - - testutils.AssertMetricsDouble(t, actualResourceMetrics[1].Metrics[3], "k8s.container.cpu_limit", - metricspb.MetricDescriptor_GAUGE_DOUBLE, 20.0) } var containerIDWithPreifx = func(containerID string) string { return "docker://" + containerID } -func TestListResourceMetrics(t *testing.T) { - rms := map[string]*agentmetricspb.ExportMetricsServiceRequest{ - "resource-1": {Resource: &resourcepb.Resource{Type: "type-1"}}, - "resource-2": {Resource: &resourcepb.Resource{Type: "type-2"}}, - "resource-3": {Resource: &resourcepb.Resource{Type: "type-1"}}, - } - - actual := listResourceMetrics(rms) - expected := []*agentmetricspb.ExportMetricsServiceRequest{ - {Resource: &resourcepb.Resource{Type: "type-1"}}, - {Resource: &resourcepb.Resource{Type: "type-2"}}, - {Resource: &resourcepb.Resource{Type: "type-1"}}, - } - - require.ElementsMatch(t, expected, actual) -} - func TestPhaseToInt(t *testing.T) { tests := []struct { name string diff --git a/receiver/k8sclusterreceiver/internal/pod/testdata/expected.yaml b/receiver/k8sclusterreceiver/internal/pod/testdata/expected.yaml new file mode 100644 index 000000000000..96b350d4d4c1 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/testdata/expected.yaml @@ -0,0 +1,87 @@ +resourceMetrics: + - resource: + attributes: + - key: k8s.namespace.name + value: + stringValue: test-namespace + - key: k8s.node.name + value: + stringValue: test-node + - key: k8s.pod.name + value: + stringValue: test-pod-1 + - key: k8s.pod.uid + value: + stringValue: test-pod-1-uid + - key: opencensus.resourcetype + value: + stringValue: k8s + schemaUrl: https://opentelemetry.io/schemas/1.18.0 + scopeMetrics: + - metrics: + - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) + gauge: + dataPoints: + - asInt: "3" + name: k8s.pod.phase + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest + - resource: + attributes: + - key: container.id + value: + stringValue: container-id + - key: container.image.name + value: + stringValue: container-image-name + - key: container.image.tag + value: + stringValue: latest + - key: k8s.container.name + value: + stringValue: container-name + - key: k8s.namespace.name + value: + stringValue: test-namespace + - key: k8s.node.name + value: + stringValue: test-node + - key: k8s.pod.name + value: + stringValue: test-pod-1 + - key: k8s.pod.uid + value: + stringValue: test-pod-1-uid + - key: opencensus.resourcetype + value: + stringValue: container + schemaUrl: https://opentelemetry.io/schemas/1.18.0 + scopeMetrics: + - metrics: + - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. + gauge: + dataPoints: + - asInt: "3" + name: k8s.container.restarts + unit: "1" + - description: Whether a container has passed its readiness probe (0 for no, 1 for yes) + gauge: + dataPoints: + - asInt: "1" + name: k8s.container.ready + unit: "1" + - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + dataPoints: + - asDouble: 10 + name: k8s.container.cpu_request + - description: Maximum resource limit set for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details + gauge: + dataPoints: + - asDouble: 20 + name: k8s.container.cpu_limit + scope: + name: otelcol/k8sclusterreceiver + version: latest \ No newline at end of file diff --git a/receiver/k8sclusterreceiver/testdata/e2e/expected.yaml b/receiver/k8sclusterreceiver/testdata/e2e/expected.yaml index b8b2f452b020..9c5b5a19c8bc 100644 --- a/receiver/k8sclusterreceiver/testdata/e2e/expected.yaml +++ b/receiver/k8sclusterreceiver/testdata/e2e/expected.yaml @@ -445,6 +445,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -453,7 +454,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -471,6 +475,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -479,7 +484,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -497,6 +505,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -505,7 +514,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -523,6 +535,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -531,7 +544,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -549,6 +565,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -557,7 +574,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -575,6 +595,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -583,7 +604,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -601,6 +625,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -609,7 +634,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -627,6 +655,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -635,7 +664,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -653,6 +685,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -661,7 +694,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: k8s.namespace.name @@ -679,6 +715,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: k8s + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: Current phase of the pod (1 - Pending, 2 - Running, 3 - Succeeded, 4 - Failed, 5 - Unknown) @@ -687,7 +724,10 @@ resourceMetrics: - asInt: "2" timeUnixNano: "1686772769034865545" name: k8s.pod.phase - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -717,6 +757,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -732,6 +773,7 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: @@ -744,7 +786,9 @@ resourceMetrics: - asInt: "104857600" timeUnixNano: "1686772769034865545" name: k8s.container.memory_request - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -774,6 +818,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -789,6 +834,7 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: @@ -813,7 +859,9 @@ resourceMetrics: - asInt: "52428800" timeUnixNano: "1686772769034865545" name: k8s.container.memory_limit - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -843,6 +891,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -858,13 +907,16 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: - asDouble: 0.25 timeUnixNano: "1686772769034865545" name: k8s.container.cpu_request - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -894,6 +946,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -909,13 +962,16 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: - asDouble: 0.1 timeUnixNano: "1686772769034865545" name: k8s.container.cpu_request - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -945,6 +1001,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -960,13 +1017,16 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: - asDouble: 0.2 timeUnixNano: "1686772769034865545" name: k8s.container.cpu_request - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -996,6 +1056,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -1011,7 +1072,10 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -1041,6 +1105,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -1056,6 +1121,7 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: @@ -1074,7 +1140,9 @@ resourceMetrics: - asInt: "178257920" timeUnixNano: "1686772769034865545" name: k8s.container.memory_limit - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -1104,6 +1172,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -1119,6 +1188,7 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: @@ -1143,7 +1213,9 @@ resourceMetrics: - asInt: "268435456" timeUnixNano: "1686772769034865545" name: k8s.container.memory_limit - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -1173,6 +1245,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -1188,7 +1261,10 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready - scope: {} + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest - resource: attributes: - key: container.id @@ -1218,6 +1294,7 @@ resourceMetrics: - key: opencensus.resourcetype value: stringValue: container + schemaUrl: "https://opentelemetry.io/schemas/1.18.0" scopeMetrics: - metrics: - description: How many times the container has restarted in the recent past. This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. @@ -1233,6 +1310,7 @@ resourceMetrics: - asInt: "1" timeUnixNano: "1686772769034865545" name: k8s.container.ready + unit: "1" - description: Resource requested for the container. See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#resourcerequirements-v1-core for details gauge: dataPoints: @@ -1251,4 +1329,6 @@ resourceMetrics: - asInt: "178257920" timeUnixNano: "1686772769034865545" name: k8s.container.memory_limit - scope: {} + scope: + name: otelcol/k8sclusterreceiver + version: latest