diff --git a/.chloggen/switchk8spodnamespacenode.yaml b/.chloggen/switchk8spodnamespacenode.yaml new file mode 100755 index 000000000000..86c35ee08aee --- /dev/null +++ b/.chloggen/switchk8spodnamespacenode.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 metrics (container, pod, node, namespace) to use pdata. + +# One or more tracking issues related to the change +issues: [23423] diff --git a/receiver/k8sclusterreceiver/e2e_test.go b/receiver/k8sclusterreceiver/e2e_test.go index ed279eb95749..2cd192663d20 100644 --- a/receiver/k8sclusterreceiver/e2e_test.go +++ b/receiver/k8sclusterreceiver/e2e_test.go @@ -82,7 +82,11 @@ func TestE2E(t *testing.T) { return value } containerImageShorten := func(value string) string { - return value[strings.LastIndex(value, "/"):] + index := strings.LastIndex(value, "/") + if index == -1 { + return value + } + return value[index:] } require.NoError(t, pmetrictest.CompareMetrics(expected, metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1], pmetrictest.IgnoreTimestamp(), diff --git a/receiver/k8sclusterreceiver/go.mod b/receiver/k8sclusterreceiver/go.mod index 031d7c30058e..be5ded43ff18 100644 --- a/receiver/k8sclusterreceiver/go.mod +++ b/receiver/k8sclusterreceiver/go.mod @@ -6,7 +6,6 @@ require ( github.com/census-instrumentation/opencensus-proto v0.4.1 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 - github.com/iancoleman/strcase v0.2.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.79.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.79.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig v0.79.0 diff --git a/receiver/k8sclusterreceiver/go.sum b/receiver/k8sclusterreceiver/go.sum index 0cb9d46bf876..75fff9744a16 100644 --- a/receiver/k8sclusterreceiver/go.sum +++ b/receiver/k8sclusterreceiver/go.sum @@ -298,8 +298,6 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= diff --git a/receiver/k8sclusterreceiver/internal/collection/collector.go b/receiver/k8sclusterreceiver/internal/collection/collector.go index d866a45f0113..1db17034ad00 100644 --- a/receiver/k8sclusterreceiver/internal/collection/collector.go +++ b/receiver/k8sclusterreceiver/internal/collection/collector.go @@ -104,11 +104,11 @@ 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 = ocsToMetrics(node.GetMetrics(o, dc.nodeConditionsToReport, dc.allocatableTypesToReport, dc.settings.TelemetrySettings.Logger)) + md = node.GetMetrics(dc.settings, o, dc.nodeConditionsToReport, dc.allocatableTypesToReport) case *corev1.Namespace: - md = ocsToMetrics(namespace.GetMetrics(o)) + md = namespace.GetMetrics(dc.settings, o) case *corev1.ReplicationController: md = ocsToMetrics(replicationcontroller.GetMetrics(o)) case *corev1.ResourceQuota: diff --git a/receiver/k8sclusterreceiver/internal/container/containers.go b/receiver/k8sclusterreceiver/internal/container/containers.go index 04f0ecabee7f..0663c11ca662 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,40 @@ 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()) + mb.RecordK8sContainerCPURequestDataPoint(ts, float64(c.Resources.Requests.Cpu().MilliValue())/1000.0) + mb.RecordK8sContainerCPULimitDataPoint(ts, float64(c.Resources.Limits.Cpu().MilliValue())/1000.0) + for _, cs := range pod.Status.ContainerStatuses { + if cs.Name == c.Name { + mb.RecordK8sContainerRestartsDataPoint(ts, int64(cs.RestartCount)) + mb.RecordK8sContainerReadyDataPoint(ts, boolToInt64(cs.Ready)) + break } } - - 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, - } -} - -// 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) + image, err := docker.ParseImageName(c.Image) if err != nil { - docker.LogParseError(err, cs.Image, logger) + docker.LogParseError(err, c.Image, set.Logger) } - - 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 + var containerID string + for _, cs := range pod.Status.ContainerStatuses { + if cs.Name == c.Name { + containerID = cs.ContainerID + } + } + return mb.Emit(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), + imetadata.WithContainerImageName(image.Repository), + imetadata.WithContainerImageTag(image.Tag), + ) } func GetMetadata(cs corev1.ContainerStatus) *metadata.KubernetesMetadata { @@ -177,3 +89,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..9893acf8ed4a --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/documentation.md @@ -0,0 +1,59 @@ +[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 | +| ---- | ----------- | ---------- | +| 1 | 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 | +| ---- | ----------- | ---------- | +| 1 | Gauge | Double | + +### 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 | + +## 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..2a2036b74aa7 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config.go @@ -0,0 +1,112 @@ +// 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"` + K8sContainerReady MetricConfig `mapstructure:"k8s.container.ready"` + K8sContainerRestarts MetricConfig `mapstructure:"k8s.container.restarts"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + K8sContainerCPULimit: MetricConfig{ + Enabled: true, + }, + K8sContainerCPURequest: MetricConfig{ + Enabled: true, + }, + K8sContainerReady: MetricConfig{ + Enabled: true, + }, + K8sContainerRestarts: 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..1e4db1ce363a --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_config_test.go @@ -0,0 +1,88 @@ +// 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}, + K8sContainerReady: MetricConfig{Enabled: true}, + K8sContainerRestarts: 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}, + K8sContainerReady: MetricConfig{Enabled: false}, + K8sContainerRestarts: 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..5cbeaf55f472 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics.go @@ -0,0 +1,431 @@ +// 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("1") + 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("1") + 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 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 +} + +// 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 + metricK8sContainerReady metricK8sContainerReady + metricK8sContainerRestarts metricK8sContainerRestarts +} + +// 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), + metricK8sContainerReady: newMetricK8sContainerReady(mbc.Metrics.K8sContainerReady), + metricK8sContainerRestarts: newMetricK8sContainerRestarts(mbc.Metrics.K8sContainerRestarts), + } + 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.metricK8sContainerReady.emit(ils.Metrics()) + mb.metricK8sContainerRestarts.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) +} + +// 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) +} + +// 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..79e55db59e9f --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/generated_metrics_test.go @@ -0,0 +1,213 @@ +// 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.RecordK8sContainerReadyDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sContainerRestartsDataPoint(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, "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.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, "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.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + 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()) + } + } + }) + } +} 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..1b2111410dd4 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/internal/metadata/testdata/config.yaml @@ -0,0 +1,59 @@ +default: +all_set: + metrics: + k8s.container.cpu_limit: + enabled: true + k8s.container.cpu_request: + enabled: true + k8s.container.ready: + enabled: true + k8s.container.restarts: + 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.ready: + enabled: false + k8s.container.restarts: + 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..7b75a6c58760 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/container/metadata.yaml @@ -0,0 +1,75 @@ +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 + unit: 1 + 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 + unit: 1 + gauge: + value_type: double + 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/namespace/doc.go b/receiver/k8sclusterreceiver/internal/namespace/doc.go new file mode 100644 index 000000000000..b3049e111e3a --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +package namespace // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/namespace" diff --git a/receiver/k8sclusterreceiver/internal/namespace/documentation.md b/receiver/k8sclusterreceiver/internal/namespace/documentation.md new file mode 100644 index 000000000000..82ceeedd546f --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/documentation.md @@ -0,0 +1,29 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# k8s/namespace + +## 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.namespace.phase + +The current phase of namespaces (1 for active and 0 for terminating) + +| 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.namespace.uid | The k8s namespace uid. | Any Str | true | +| opencensus.resourcetype | The OpenCensus resource type. | Any Str | true | diff --git a/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_config.go b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_config.go new file mode 100644 index 000000000000..aaf8839aba9b --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_config.go @@ -0,0 +1,76 @@ +// 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/namespace metrics. +type MetricsConfig struct { + K8sNamespacePhase MetricConfig `mapstructure:"k8s.namespace.phase"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + K8sNamespacePhase: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// ResourceAttributesConfig provides config for k8s/namespace resource attributes. +type ResourceAttributesConfig struct { + K8sNamespaceName ResourceAttributeConfig `mapstructure:"k8s.namespace.name"` + K8sNamespaceUID ResourceAttributeConfig `mapstructure:"k8s.namespace.uid"` + OpencensusResourcetype ResourceAttributeConfig `mapstructure:"opencensus.resourcetype"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sNamespaceUID: ResourceAttributeConfig{ + Enabled: true, + }, + OpencensusResourcetype: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for k8s/namespace 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/namespace/internal/metadata/generated_config_test.go b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_config_test.go new file mode 100644 index 000000000000..46a3b57d9192 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_config_test.go @@ -0,0 +1,70 @@ +// 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{ + K8sNamespacePhase: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{Enabled: true}, + K8sNamespaceUID: ResourceAttributeConfig{Enabled: true}, + OpencensusResourcetype: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + K8sNamespacePhase: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNamespaceName: ResourceAttributeConfig{Enabled: false}, + K8sNamespaceUID: 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/namespace/internal/metadata/generated_metrics.go b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_metrics.go new file mode 100644 index 000000000000..bc691162ce3a --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_metrics.go @@ -0,0 +1,206 @@ +// 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 metricK8sNamespacePhase 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.namespace.phase metric with initial data. +func (m *metricK8sNamespacePhase) init() { + m.data.SetName("k8s.namespace.phase") + m.data.SetDescription("The current phase of namespaces (1 for active and 0 for terminating)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNamespacePhase) 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 *metricK8sNamespacePhase) 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 *metricK8sNamespacePhase) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNamespacePhase(cfg MetricConfig) metricK8sNamespacePhase { + m := metricK8sNamespacePhase{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 + metricK8sNamespacePhase metricK8sNamespacePhase +} + +// 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, + metricK8sNamespacePhase: newMetricK8sNamespacePhase(mbc.Metrics.K8sNamespacePhase), + } + 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) + } + } +} + +// WithK8sNamespaceUID sets provided value as "k8s.namespace.uid" attribute for current resource. +func WithK8sNamespaceUID(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNamespaceUID.Enabled { + rm.Resource().Attributes().PutStr("k8s.namespace.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.metricK8sNamespacePhase.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 +} + +// RecordK8sNamespacePhaseDataPoint adds a data point to k8s.namespace.phase metric. +func (mb *MetricsBuilder) RecordK8sNamespacePhaseDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNamespacePhase.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/namespace/internal/metadata/generated_metrics_test.go b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_metrics_test.go new file mode 100644 index 000000000000..256b3f77fecf --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/generated_metrics_test.go @@ -0,0 +1,123 @@ +// 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.RecordK8sNamespacePhaseDataPoint(ts, 1) + + metrics := mb.Emit(WithK8sNamespaceName("attr-val"), WithK8sNamespaceUID("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.namespace.uid") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNamespaceUID.Enabled, ok) + if mb.resourceAttributesConfig.K8sNamespaceUID.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, 3) + + 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.namespace.phase": + assert.False(t, validatedMetrics["k8s.namespace.phase"], "Found a duplicate in the metrics slice: k8s.namespace.phase") + validatedMetrics["k8s.namespace.phase"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "The current phase of namespaces (1 for active and 0 for terminating)", 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/namespace/internal/metadata/testdata/config.yaml b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/testdata/config.yaml new file mode 100644 index 000000000000..e9afe02e3aac --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/internal/metadata/testdata/config.yaml @@ -0,0 +1,23 @@ +default: +all_set: + metrics: + k8s.namespace.phase: + enabled: true + resource_attributes: + k8s.namespace.name: + enabled: true + k8s.namespace.uid: + enabled: true + opencensus.resourcetype: + enabled: true +none_set: + metrics: + k8s.namespace.phase: + enabled: false + resource_attributes: + k8s.namespace.name: + enabled: false + k8s.namespace.uid: + enabled: false + opencensus.resourcetype: + enabled: false diff --git a/receiver/k8sclusterreceiver/internal/namespace/metadata.yaml b/receiver/k8sclusterreceiver/internal/namespace/metadata.yaml new file mode 100644 index 000000000000..0cc0ec1c48f0 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/metadata.yaml @@ -0,0 +1,27 @@ +type: k8s/namespace + +sem_conv_version: 1.18.0 + +resource_attributes: + k8s.namespace.uid: + description: The k8s namespace uid. + type: string + enabled: true + + k8s.namespace.name: + description: The k8s namespace name. + type: string + enabled: true + + opencensus.resourcetype: + description: The OpenCensus resource type. + type: string + enabled: true + +metrics: + k8s.namespace.phase: + enabled: true + description: The current phase of namespaces (1 for active and 0 for terminating) + unit: 1 + gauge: + value_type: int \ No newline at end of file diff --git a/receiver/k8sclusterreceiver/internal/namespace/namespaces.go b/receiver/k8sclusterreceiver/internal/namespace/namespaces.go index 2daef0a2f2b5..833f4d4f9087 100644 --- a/receiver/k8sclusterreceiver/internal/namespace/namespaces.go +++ b/receiver/k8sclusterreceiver/internal/namespace/namespaces.go @@ -4,46 +4,21 @@ package namespace // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/namespace" import ( - 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" - conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" corev1 "k8s.io/api/core/v1" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/utils" + imetadata "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/namespace/internal/metadata" ) -func GetMetrics(ns *corev1.Namespace) []*agentmetricspb.ExportMetricsServiceRequest { - metrics := []*metricspb.Metric{ - { - MetricDescriptor: &metricspb.MetricDescriptor{ - Name: "k8s.namespace.phase", - Description: "The current phase of namespaces (1 for active and 0 for terminating)", - Type: metricspb.MetricDescriptor_GAUGE_INT64, - }, - Timeseries: []*metricspb.TimeSeries{ - utils.GetInt64TimeSeries(int64(namespacePhaseValues[ns.Status.Phase])), - }, - }, - } - - return []*agentmetricspb.ExportMetricsServiceRequest{ - { - Resource: getResource(ns), - Metrics: metrics, - }, - } -} - -func getResource(ns *corev1.Namespace) *resourcepb.Resource { - return &resourcepb.Resource{ - Type: constants.K8sType, - Labels: map[string]string{ - constants.K8sKeyNamespaceUID: string(ns.UID), - conventions.AttributeK8SNamespaceName: ns.Namespace, - }, - } +func GetMetrics(set receiver.CreateSettings, ns *corev1.Namespace) pmetric.Metrics { + mb := imetadata.NewMetricsBuilder(imetadata.DefaultMetricsBuilderConfig(), set) + ts := pcommon.NewTimestampFromTime(time.Now()) + mb.RecordK8sNamespacePhaseDataPoint(ts, int64(namespacePhaseValues[ns.Status.Phase])) + return mb.Emit(imetadata.WithK8sNamespaceUID(string(ns.UID)), imetadata.WithK8sNamespaceName(ns.Namespace), imetadata.WithOpencensusResourcetype("k8s")) } var namespacePhaseValues = map[corev1.NamespacePhase]int32{ diff --git a/receiver/k8sclusterreceiver/internal/namespace/namespaces_test.go b/receiver/k8sclusterreceiver/internal/namespace/namespaces_test.go index 8d8723bea158..7c839aa756e9 100644 --- a/receiver/k8sclusterreceiver/internal/namespace/namespaces_test.go +++ b/receiver/k8sclusterreceiver/internal/namespace/namespaces_test.go @@ -4,35 +4,33 @@ package namespace import ( + "path/filepath" "testing" - metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/receiver/receivertest" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/testutils" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/golden" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest" ) func TestNamespaceMetrics(t *testing.T) { n := newNamespace("1") - - actualResourceMetrics := GetMetrics(n) - - require.Equal(t, 1, len(actualResourceMetrics)) - - require.Equal(t, 1, len(actualResourceMetrics[0].Metrics)) - testutils.AssertResource(t, actualResourceMetrics[0].Resource, constants.K8sType, - map[string]string{ - "k8s.namespace.uid": "test-namespace-1-uid", - "k8s.namespace.name": "test-namespace", - }, + m := GetMetrics(receivertest.NewNopCreateSettings(), n) + + 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[0].Metrics[0], "k8s.namespace.phase", - metricspb.MetricDescriptor_GAUGE_INT64, 0) } func newNamespace(id string) *corev1.Namespace { diff --git a/receiver/k8sclusterreceiver/internal/namespace/testdata/expected.yaml b/receiver/k8sclusterreceiver/internal/namespace/testdata/expected.yaml new file mode 100644 index 000000000000..383bbce3f774 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/namespace/testdata/expected.yaml @@ -0,0 +1,24 @@ +resourceMetrics: + - resource: + attributes: + - key: k8s.namespace.name + value: + stringValue: test-namespace + - key: k8s.namespace.uid + value: + stringValue: test-namespace-1-uid + - key: opencensus.resourcetype + value: + stringValue: k8s + schemaUrl: https://opentelemetry.io/schemas/1.18.0 + scopeMetrics: + - metrics: + - description: The current phase of namespaces (1 for active and 0 for terminating) + gauge: + dataPoints: + - asInt: "0" + name: k8s.namespace.phase + unit: "1" + scope: + name: otelcol/k8sclusterreceiver + version: latest diff --git a/receiver/k8sclusterreceiver/internal/node/doc.go b/receiver/k8sclusterreceiver/internal/node/doc.go new file mode 100644 index 000000000000..5e313b2c2628 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +package node // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/node" diff --git a/receiver/k8sclusterreceiver/internal/node/documentation.md b/receiver/k8sclusterreceiver/internal/node/documentation.md new file mode 100644 index 000000000000..d3098eec3008 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/documentation.md @@ -0,0 +1,85 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# k8s/node + +## 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.node.allocatable_cpu + +How many CPU cores remaining that the node can allocate to pods + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| {cores} | Gauge | Double | + +### k8s.node.allocatable_ephemeral_storage + +How many bytes of ephemeral storage remaining that the node can allocate to pods + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| By | Gauge | Int | + +### k8s.node.allocatable_memory + +How many bytes of RAM memory remaining that the node can allocate to pods + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| By | Gauge | Int | + +### k8s.node.condition_disk_pressure + +Whether this node is DiskPressure (1), not DiskPressure (0) or in an unknown state (-1) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.node.condition_memory_pressure + +Whether this node is MemoryPressure (1), not MemoryPressure (0) or in an unknown state (-1) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.node.condition_network_unavailable + +Whether this node is NetworkUnavailable (1), not NetworkUnavailable (0) or in an unknown state (-1) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.node.condition_pid_pressure + +Whether this node is PidPressure (1), not PidPressure (0) or in an unknown state (-1) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +### k8s.node.condition_ready + +Whether this node is Ready (1), not Ready (0) or in an unknown state (-1) + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Int | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| k8s.node.name | The k8s node name. | Any Str | true | +| k8s.node.uid | The k8s node uid. | Any Str | true | +| opencensus.resourcetype | The OpenCensus resource type. | Any Str | true | diff --git a/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_config.go b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_config.go new file mode 100644 index 000000000000..2ac78d6e371c --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_config.go @@ -0,0 +1,104 @@ +// 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/node metrics. +type MetricsConfig struct { + K8sNodeAllocatableCPU MetricConfig `mapstructure:"k8s.node.allocatable_cpu"` + K8sNodeAllocatableEphemeralStorage MetricConfig `mapstructure:"k8s.node.allocatable_ephemeral_storage"` + K8sNodeAllocatableMemory MetricConfig `mapstructure:"k8s.node.allocatable_memory"` + K8sNodeConditionDiskPressure MetricConfig `mapstructure:"k8s.node.condition_disk_pressure"` + K8sNodeConditionMemoryPressure MetricConfig `mapstructure:"k8s.node.condition_memory_pressure"` + K8sNodeConditionNetworkUnavailable MetricConfig `mapstructure:"k8s.node.condition_network_unavailable"` + K8sNodeConditionPidPressure MetricConfig `mapstructure:"k8s.node.condition_pid_pressure"` + K8sNodeConditionReady MetricConfig `mapstructure:"k8s.node.condition_ready"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + K8sNodeAllocatableCPU: MetricConfig{ + Enabled: true, + }, + K8sNodeAllocatableEphemeralStorage: MetricConfig{ + Enabled: true, + }, + K8sNodeAllocatableMemory: MetricConfig{ + Enabled: true, + }, + K8sNodeConditionDiskPressure: MetricConfig{ + Enabled: true, + }, + K8sNodeConditionMemoryPressure: MetricConfig{ + Enabled: true, + }, + K8sNodeConditionNetworkUnavailable: MetricConfig{ + Enabled: true, + }, + K8sNodeConditionPidPressure: MetricConfig{ + Enabled: true, + }, + K8sNodeConditionReady: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// ResourceAttributesConfig provides config for k8s/node resource attributes. +type ResourceAttributesConfig struct { + K8sNodeName ResourceAttributeConfig `mapstructure:"k8s.node.name"` + K8sNodeUID ResourceAttributeConfig `mapstructure:"k8s.node.uid"` + OpencensusResourcetype ResourceAttributeConfig `mapstructure:"opencensus.resourcetype"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + K8sNodeName: ResourceAttributeConfig{ + Enabled: true, + }, + K8sNodeUID: ResourceAttributeConfig{ + Enabled: true, + }, + OpencensusResourcetype: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for k8s/node 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/node/internal/metadata/generated_config_test.go b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_config_test.go new file mode 100644 index 000000000000..03dd067a93f6 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_config_test.go @@ -0,0 +1,84 @@ +// 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{ + K8sNodeAllocatableCPU: MetricConfig{Enabled: true}, + K8sNodeAllocatableEphemeralStorage: MetricConfig{Enabled: true}, + K8sNodeAllocatableMemory: MetricConfig{Enabled: true}, + K8sNodeConditionDiskPressure: MetricConfig{Enabled: true}, + K8sNodeConditionMemoryPressure: MetricConfig{Enabled: true}, + K8sNodeConditionNetworkUnavailable: MetricConfig{Enabled: true}, + K8sNodeConditionPidPressure: MetricConfig{Enabled: true}, + K8sNodeConditionReady: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNodeName: ResourceAttributeConfig{Enabled: true}, + K8sNodeUID: ResourceAttributeConfig{Enabled: true}, + OpencensusResourcetype: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + K8sNodeAllocatableCPU: MetricConfig{Enabled: false}, + K8sNodeAllocatableEphemeralStorage: MetricConfig{Enabled: false}, + K8sNodeAllocatableMemory: MetricConfig{Enabled: false}, + K8sNodeConditionDiskPressure: MetricConfig{Enabled: false}, + K8sNodeConditionMemoryPressure: MetricConfig{Enabled: false}, + K8sNodeConditionNetworkUnavailable: MetricConfig{Enabled: false}, + K8sNodeConditionPidPressure: MetricConfig{Enabled: false}, + K8sNodeConditionReady: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + K8sNodeName: ResourceAttributeConfig{Enabled: false}, + K8sNodeUID: 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/node/internal/metadata/generated_metrics.go b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_metrics.go new file mode 100644 index 000000000000..c51a21f76e3a --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_metrics.go @@ -0,0 +1,605 @@ +// 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 metricK8sNodeAllocatableCPU 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.node.allocatable_cpu metric with initial data. +func (m *metricK8sNodeAllocatableCPU) init() { + m.data.SetName("k8s.node.allocatable_cpu") + m.data.SetDescription("How many CPU cores remaining that the node can allocate to pods") + m.data.SetUnit("{cores}") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeAllocatableCPU) 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 *metricK8sNodeAllocatableCPU) 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 *metricK8sNodeAllocatableCPU) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeAllocatableCPU(cfg MetricConfig) metricK8sNodeAllocatableCPU { + m := metricK8sNodeAllocatableCPU{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeAllocatableEphemeralStorage 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.node.allocatable_ephemeral_storage metric with initial data. +func (m *metricK8sNodeAllocatableEphemeralStorage) init() { + m.data.SetName("k8s.node.allocatable_ephemeral_storage") + m.data.SetDescription("How many bytes of ephemeral storage remaining that the node can allocate to pods") + m.data.SetUnit("By") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeAllocatableEphemeralStorage) 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 *metricK8sNodeAllocatableEphemeralStorage) 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 *metricK8sNodeAllocatableEphemeralStorage) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeAllocatableEphemeralStorage(cfg MetricConfig) metricK8sNodeAllocatableEphemeralStorage { + m := metricK8sNodeAllocatableEphemeralStorage{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeAllocatableMemory 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.node.allocatable_memory metric with initial data. +func (m *metricK8sNodeAllocatableMemory) init() { + m.data.SetName("k8s.node.allocatable_memory") + m.data.SetDescription("How many bytes of RAM memory remaining that the node can allocate to pods") + m.data.SetUnit("By") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeAllocatableMemory) 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 *metricK8sNodeAllocatableMemory) 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 *metricK8sNodeAllocatableMemory) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeAllocatableMemory(cfg MetricConfig) metricK8sNodeAllocatableMemory { + m := metricK8sNodeAllocatableMemory{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeConditionDiskPressure 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.node.condition_disk_pressure metric with initial data. +func (m *metricK8sNodeConditionDiskPressure) init() { + m.data.SetName("k8s.node.condition_disk_pressure") + m.data.SetDescription("Whether this node is DiskPressure (1), not DiskPressure (0) or in an unknown state (-1)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeConditionDiskPressure) 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 *metricK8sNodeConditionDiskPressure) 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 *metricK8sNodeConditionDiskPressure) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeConditionDiskPressure(cfg MetricConfig) metricK8sNodeConditionDiskPressure { + m := metricK8sNodeConditionDiskPressure{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeConditionMemoryPressure 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.node.condition_memory_pressure metric with initial data. +func (m *metricK8sNodeConditionMemoryPressure) init() { + m.data.SetName("k8s.node.condition_memory_pressure") + m.data.SetDescription("Whether this node is MemoryPressure (1), not MemoryPressure (0) or in an unknown state (-1)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeConditionMemoryPressure) 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 *metricK8sNodeConditionMemoryPressure) 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 *metricK8sNodeConditionMemoryPressure) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeConditionMemoryPressure(cfg MetricConfig) metricK8sNodeConditionMemoryPressure { + m := metricK8sNodeConditionMemoryPressure{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeConditionNetworkUnavailable 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.node.condition_network_unavailable metric with initial data. +func (m *metricK8sNodeConditionNetworkUnavailable) init() { + m.data.SetName("k8s.node.condition_network_unavailable") + m.data.SetDescription("Whether this node is NetworkUnavailable (1), not NetworkUnavailable (0) or in an unknown state (-1)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeConditionNetworkUnavailable) 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 *metricK8sNodeConditionNetworkUnavailable) 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 *metricK8sNodeConditionNetworkUnavailable) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeConditionNetworkUnavailable(cfg MetricConfig) metricK8sNodeConditionNetworkUnavailable { + m := metricK8sNodeConditionNetworkUnavailable{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeConditionPidPressure 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.node.condition_pid_pressure metric with initial data. +func (m *metricK8sNodeConditionPidPressure) init() { + m.data.SetName("k8s.node.condition_pid_pressure") + m.data.SetDescription("Whether this node is PidPressure (1), not PidPressure (0) or in an unknown state (-1)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeConditionPidPressure) 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 *metricK8sNodeConditionPidPressure) 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 *metricK8sNodeConditionPidPressure) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeConditionPidPressure(cfg MetricConfig) metricK8sNodeConditionPidPressure { + m := metricK8sNodeConditionPidPressure{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricK8sNodeConditionReady 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.node.condition_ready metric with initial data. +func (m *metricK8sNodeConditionReady) init() { + m.data.SetName("k8s.node.condition_ready") + m.data.SetDescription("Whether this node is Ready (1), not Ready (0) or in an unknown state (-1)") + m.data.SetUnit("1") + m.data.SetEmptyGauge() +} + +func (m *metricK8sNodeConditionReady) 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 *metricK8sNodeConditionReady) 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 *metricK8sNodeConditionReady) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricK8sNodeConditionReady(cfg MetricConfig) metricK8sNodeConditionReady { + m := metricK8sNodeConditionReady{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 + metricK8sNodeAllocatableCPU metricK8sNodeAllocatableCPU + metricK8sNodeAllocatableEphemeralStorage metricK8sNodeAllocatableEphemeralStorage + metricK8sNodeAllocatableMemory metricK8sNodeAllocatableMemory + metricK8sNodeConditionDiskPressure metricK8sNodeConditionDiskPressure + metricK8sNodeConditionMemoryPressure metricK8sNodeConditionMemoryPressure + metricK8sNodeConditionNetworkUnavailable metricK8sNodeConditionNetworkUnavailable + metricK8sNodeConditionPidPressure metricK8sNodeConditionPidPressure + metricK8sNodeConditionReady metricK8sNodeConditionReady +} + +// 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, + metricK8sNodeAllocatableCPU: newMetricK8sNodeAllocatableCPU(mbc.Metrics.K8sNodeAllocatableCPU), + metricK8sNodeAllocatableEphemeralStorage: newMetricK8sNodeAllocatableEphemeralStorage(mbc.Metrics.K8sNodeAllocatableEphemeralStorage), + metricK8sNodeAllocatableMemory: newMetricK8sNodeAllocatableMemory(mbc.Metrics.K8sNodeAllocatableMemory), + metricK8sNodeConditionDiskPressure: newMetricK8sNodeConditionDiskPressure(mbc.Metrics.K8sNodeConditionDiskPressure), + metricK8sNodeConditionMemoryPressure: newMetricK8sNodeConditionMemoryPressure(mbc.Metrics.K8sNodeConditionMemoryPressure), + metricK8sNodeConditionNetworkUnavailable: newMetricK8sNodeConditionNetworkUnavailable(mbc.Metrics.K8sNodeConditionNetworkUnavailable), + metricK8sNodeConditionPidPressure: newMetricK8sNodeConditionPidPressure(mbc.Metrics.K8sNodeConditionPidPressure), + metricK8sNodeConditionReady: newMetricK8sNodeConditionReady(mbc.Metrics.K8sNodeConditionReady), + } + 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) + +// 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) + } + } +} + +// WithK8sNodeUID sets provided value as "k8s.node.uid" attribute for current resource. +func WithK8sNodeUID(val string) ResourceMetricsOption { + return func(rac ResourceAttributesConfig, rm pmetric.ResourceMetrics) { + if rac.K8sNodeUID.Enabled { + rm.Resource().Attributes().PutStr("k8s.node.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.metricK8sNodeAllocatableCPU.emit(ils.Metrics()) + mb.metricK8sNodeAllocatableEphemeralStorage.emit(ils.Metrics()) + mb.metricK8sNodeAllocatableMemory.emit(ils.Metrics()) + mb.metricK8sNodeConditionDiskPressure.emit(ils.Metrics()) + mb.metricK8sNodeConditionMemoryPressure.emit(ils.Metrics()) + mb.metricK8sNodeConditionNetworkUnavailable.emit(ils.Metrics()) + mb.metricK8sNodeConditionPidPressure.emit(ils.Metrics()) + mb.metricK8sNodeConditionReady.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 +} + +// RecordK8sNodeAllocatableCPUDataPoint adds a data point to k8s.node.allocatable_cpu metric. +func (mb *MetricsBuilder) RecordK8sNodeAllocatableCPUDataPoint(ts pcommon.Timestamp, val float64) { + mb.metricK8sNodeAllocatableCPU.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeAllocatableEphemeralStorageDataPoint adds a data point to k8s.node.allocatable_ephemeral_storage metric. +func (mb *MetricsBuilder) RecordK8sNodeAllocatableEphemeralStorageDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeAllocatableEphemeralStorage.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeAllocatableMemoryDataPoint adds a data point to k8s.node.allocatable_memory metric. +func (mb *MetricsBuilder) RecordK8sNodeAllocatableMemoryDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeAllocatableMemory.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeConditionDiskPressureDataPoint adds a data point to k8s.node.condition_disk_pressure metric. +func (mb *MetricsBuilder) RecordK8sNodeConditionDiskPressureDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeConditionDiskPressure.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeConditionMemoryPressureDataPoint adds a data point to k8s.node.condition_memory_pressure metric. +func (mb *MetricsBuilder) RecordK8sNodeConditionMemoryPressureDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeConditionMemoryPressure.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeConditionNetworkUnavailableDataPoint adds a data point to k8s.node.condition_network_unavailable metric. +func (mb *MetricsBuilder) RecordK8sNodeConditionNetworkUnavailableDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeConditionNetworkUnavailable.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeConditionPidPressureDataPoint adds a data point to k8s.node.condition_pid_pressure metric. +func (mb *MetricsBuilder) RecordK8sNodeConditionPidPressureDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeConditionPidPressure.recordDataPoint(mb.startTime, ts, val) +} + +// RecordK8sNodeConditionReadyDataPoint adds a data point to k8s.node.condition_ready metric. +func (mb *MetricsBuilder) RecordK8sNodeConditionReadyDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricK8sNodeConditionReady.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/node/internal/metadata/generated_metrics_test.go b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_metrics_test.go new file mode 100644 index 000000000000..960839a54e6d --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/internal/metadata/generated_metrics_test.go @@ -0,0 +1,235 @@ +// 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.RecordK8sNodeAllocatableCPUDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeAllocatableEphemeralStorageDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeAllocatableMemoryDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeConditionDiskPressureDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeConditionMemoryPressureDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeConditionNetworkUnavailableDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeConditionPidPressureDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordK8sNodeConditionReadyDataPoint(ts, 1) + + metrics := mb.Emit(WithK8sNodeName("attr-val"), WithK8sNodeUID("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.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.node.uid") + attrCount++ + assert.Equal(t, mb.resourceAttributesConfig.K8sNodeUID.Enabled, ok) + if mb.resourceAttributesConfig.K8sNodeUID.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, 3) + + 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.node.allocatable_cpu": + assert.False(t, validatedMetrics["k8s.node.allocatable_cpu"], "Found a duplicate in the metrics slice: k8s.node.allocatable_cpu") + validatedMetrics["k8s.node.allocatable_cpu"] = 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 CPU cores remaining that the node can allocate to pods", ms.At(i).Description()) + assert.Equal(t, "{cores}", 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.node.allocatable_ephemeral_storage": + assert.False(t, validatedMetrics["k8s.node.allocatable_ephemeral_storage"], "Found a duplicate in the metrics slice: k8s.node.allocatable_ephemeral_storage") + validatedMetrics["k8s.node.allocatable_ephemeral_storage"] = 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 bytes of ephemeral storage remaining that the node can allocate to pods", ms.At(i).Description()) + assert.Equal(t, "By", 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.node.allocatable_memory": + assert.False(t, validatedMetrics["k8s.node.allocatable_memory"], "Found a duplicate in the metrics slice: k8s.node.allocatable_memory") + validatedMetrics["k8s.node.allocatable_memory"] = 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 bytes of RAM memory remaining that the node can allocate to pods", ms.At(i).Description()) + assert.Equal(t, "By", 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.node.condition_disk_pressure": + assert.False(t, validatedMetrics["k8s.node.condition_disk_pressure"], "Found a duplicate in the metrics slice: k8s.node.condition_disk_pressure") + validatedMetrics["k8s.node.condition_disk_pressure"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Whether this node is DiskPressure (1), not DiskPressure (0) or in an unknown state (-1)", 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.node.condition_memory_pressure": + assert.False(t, validatedMetrics["k8s.node.condition_memory_pressure"], "Found a duplicate in the metrics slice: k8s.node.condition_memory_pressure") + validatedMetrics["k8s.node.condition_memory_pressure"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Whether this node is MemoryPressure (1), not MemoryPressure (0) or in an unknown state (-1)", 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.node.condition_network_unavailable": + assert.False(t, validatedMetrics["k8s.node.condition_network_unavailable"], "Found a duplicate in the metrics slice: k8s.node.condition_network_unavailable") + validatedMetrics["k8s.node.condition_network_unavailable"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Whether this node is NetworkUnavailable (1), not NetworkUnavailable (0) or in an unknown state (-1)", 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.node.condition_pid_pressure": + assert.False(t, validatedMetrics["k8s.node.condition_pid_pressure"], "Found a duplicate in the metrics slice: k8s.node.condition_pid_pressure") + validatedMetrics["k8s.node.condition_pid_pressure"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Whether this node is PidPressure (1), not PidPressure (0) or in an unknown state (-1)", 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.node.condition_ready": + assert.False(t, validatedMetrics["k8s.node.condition_ready"], "Found a duplicate in the metrics slice: k8s.node.condition_ready") + validatedMetrics["k8s.node.condition_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 this node is Ready (1), not Ready (0) or in an unknown state (-1)", 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/node/internal/metadata/testdata/config.yaml b/receiver/k8sclusterreceiver/internal/node/internal/metadata/testdata/config.yaml new file mode 100644 index 000000000000..7877459bc728 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/internal/metadata/testdata/config.yaml @@ -0,0 +1,51 @@ +default: +all_set: + metrics: + k8s.node.allocatable_cpu: + enabled: true + k8s.node.allocatable_ephemeral_storage: + enabled: true + k8s.node.allocatable_memory: + enabled: true + k8s.node.condition_disk_pressure: + enabled: true + k8s.node.condition_memory_pressure: + enabled: true + k8s.node.condition_network_unavailable: + enabled: true + k8s.node.condition_pid_pressure: + enabled: true + k8s.node.condition_ready: + enabled: true + resource_attributes: + k8s.node.name: + enabled: true + k8s.node.uid: + enabled: true + opencensus.resourcetype: + enabled: true +none_set: + metrics: + k8s.node.allocatable_cpu: + enabled: false + k8s.node.allocatable_ephemeral_storage: + enabled: false + k8s.node.allocatable_memory: + enabled: false + k8s.node.condition_disk_pressure: + enabled: false + k8s.node.condition_memory_pressure: + enabled: false + k8s.node.condition_network_unavailable: + enabled: false + k8s.node.condition_pid_pressure: + enabled: false + k8s.node.condition_ready: + enabled: false + resource_attributes: + k8s.node.name: + enabled: false + k8s.node.uid: + enabled: false + opencensus.resourcetype: + enabled: false diff --git a/receiver/k8sclusterreceiver/internal/node/metadata.yaml b/receiver/k8sclusterreceiver/internal/node/metadata.yaml new file mode 100644 index 000000000000..e06b4a187cbd --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/metadata.yaml @@ -0,0 +1,69 @@ +type: k8s/node + +sem_conv_version: 1.18.0 + +resource_attributes: + k8s.node.uid: + description: The k8s node uid. + type: string + enabled: true + + k8s.node.name: + description: The k8s node name. + type: string + enabled: true + + opencensus.resourcetype: + description: The OpenCensus resource type. + type: string + enabled: true + +metrics: + k8s.node.condition_ready: + enabled: true + description: Whether this node is Ready (1), not Ready (0) or in an unknown state (-1) + unit: 1 + gauge: + value_type: int + k8s.node.condition_memory_pressure: + enabled: true + description: Whether this node is MemoryPressure (1), not MemoryPressure (0) or in an unknown state (-1) + unit: 1 + gauge: + value_type: int + k8s.node.condition_disk_pressure: + enabled: true + description: Whether this node is DiskPressure (1), not DiskPressure (0) or in an unknown state (-1) + unit: 1 + gauge: + value_type: int + k8s.node.condition_pid_pressure: + enabled: true + description: Whether this node is PidPressure (1), not PidPressure (0) or in an unknown state (-1) + unit: 1 + gauge: + value_type: int + k8s.node.condition_network_unavailable: + enabled: true + description: Whether this node is NetworkUnavailable (1), not NetworkUnavailable (0) or in an unknown state (-1) + unit: 1 + gauge: + value_type: int + k8s.node.allocatable_cpu: + enabled: true + description: How many CPU cores remaining that the node can allocate to pods + unit: "{cores}" + gauge: + value_type: double + k8s.node.allocatable_memory: + enabled: true + description: How many bytes of RAM memory remaining that the node can allocate to pods + unit: "By" + gauge: + value_type: int + k8s.node.allocatable_ephemeral_storage: + enabled: true + description: How many bytes of ephemeral storage remaining that the node can allocate to pods + unit: "By" + gauge: + value_type: int \ No newline at end of file diff --git a/receiver/k8sclusterreceiver/internal/node/nodes.go b/receiver/k8sclusterreceiver/internal/node/nodes.go index 90c0411268ab..527fe667fb0a 100644 --- a/receiver/k8sclusterreceiver/internal/node/nodes.go +++ b/receiver/k8sclusterreceiver/internal/node/nodes.go @@ -7,10 +7,9 @@ import ( "fmt" "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/iancoleman/strcase" + "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" @@ -18,9 +17,8 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/maps" "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/receiver/k8sclusterreceiver/internal/metadata" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/utils" + imetadata "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/node/internal/metadata" ) const ( @@ -28,13 +26,6 @@ const ( nodeCreationTime = "node.creation_timestamp" ) -var allocatableDesciption = map[string]string{ - "cpu": "How many CPU cores remaining that the node can allocate to pods", - "memory": "How many bytes of RAM memory remaining that the node can allocate to pods", - "ephemeral-storage": "How many bytes of ephemeral storage remaining that the node can allocate to pods", - "storage": "How many bytes of storage remaining that the node can allocate to pods", -} - // Transform transforms the node to remove the fields that we don't use to reduce RAM utilization. // IMPORTANT: Make sure to update this function when using a new node fields. func Transform(node *corev1.Node) *corev1.Node { @@ -57,79 +48,54 @@ func Transform(node *corev1.Node) *corev1.Node { return newNode } -func GetMetrics(node *corev1.Node, nodeConditionTypesToReport, allocatableTypesToReport []string, logger *zap.Logger) []*agentmetricspb.ExportMetricsServiceRequest { - metrics := make([]*metricspb.Metric, 0, len(nodeConditionTypesToReport)+len(allocatableTypesToReport)) +func GetMetrics(set receiver.CreateSettings, node *corev1.Node, nodeConditionTypesToReport, allocatableTypesToReport []string) pmetric.Metrics { + mb := imetadata.NewMetricsBuilder(imetadata.DefaultMetricsBuilderConfig(), set) + ts := pcommon.NewTimestampFromTime(time.Now()) + // Adding 'node condition type' metrics for _, nodeConditionTypeValue := range nodeConditionTypesToReport { - nodeConditionMetric := getNodeConditionMetric(nodeConditionTypeValue) - v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) - - metrics = append(metrics, &metricspb.Metric{ - MetricDescriptor: &metricspb.MetricDescriptor{ - Name: nodeConditionMetric, - Description: fmt.Sprintf("Whether this node is %s (1), "+ - "not %s (0) or in an unknown state (-1)", nodeConditionTypeValue, nodeConditionTypeValue), - Type: metricspb.MetricDescriptor_GAUGE_INT64, - }, - Timeseries: []*metricspb.TimeSeries{ - utils.GetInt64TimeSeries(nodeConditionValue(node, v1NodeConditionTypeValue)), - }, - }) + switch nodeConditionTypeValue { + case "Ready": + v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) + mb.RecordK8sNodeConditionReadyDataPoint(ts, nodeConditionValue(node, v1NodeConditionTypeValue)) + case "MemoryPressure": + v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) + mb.RecordK8sNodeConditionMemoryPressureDataPoint(ts, nodeConditionValue(node, v1NodeConditionTypeValue)) + case "DiskPressure": + v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) + mb.RecordK8sNodeConditionDiskPressureDataPoint(ts, nodeConditionValue(node, v1NodeConditionTypeValue)) + case "NetworkUnavailable": + v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) + mb.RecordK8sNodeConditionNetworkUnavailableDataPoint(ts, nodeConditionValue(node, v1NodeConditionTypeValue)) + case "PIDPressure": + v1NodeConditionTypeValue := corev1.NodeConditionType(nodeConditionTypeValue) + mb.RecordK8sNodeConditionPidPressureDataPoint(ts, nodeConditionValue(node, v1NodeConditionTypeValue)) + default: + set.Logger.Warn("unknown node condition type", zap.String("conditionType", nodeConditionTypeValue)) + } } // Adding 'node allocatable type' metrics for _, nodeAllocatableTypeValue := range allocatableTypesToReport { - nodeAllocatableMetric := getNodeAllocatableMetric(nodeAllocatableTypeValue) v1NodeAllocatableTypeValue := corev1.ResourceName(nodeAllocatableTypeValue) - valType := metricspb.MetricDescriptor_GAUGE_INT64 quantity, ok := node.Status.Allocatable[v1NodeAllocatableTypeValue] if !ok { - logger.Debug(fmt.Errorf("allocatable type %v not found in node %v", nodeAllocatableTypeValue, + set.Logger.Debug(fmt.Errorf("allocatable type %v not found in node %v", nodeAllocatableTypeValue, node.GetName()).Error()) continue } - val := utils.GetInt64TimeSeries(quantity.Value()) - if v1NodeAllocatableTypeValue == corev1.ResourceCPU { + switch v1NodeAllocatableTypeValue { + case corev1.ResourceCPU: // cpu metrics must be of the double type to adhere to opentelemetry system.cpu metric specifications - val = utils.GetDoubleTimeSeries(float64(quantity.MilliValue()) / 1000.0) - valType = metricspb.MetricDescriptor_GAUGE_DOUBLE + mb.RecordK8sNodeAllocatableCPUDataPoint(ts, float64(quantity.MilliValue())/1000.0) + case corev1.ResourceMemory: + mb.RecordK8sNodeAllocatableMemoryDataPoint(ts, quantity.Value()) + case corev1.ResourceEphemeralStorage: + mb.RecordK8sNodeAllocatableEphemeralStorageDataPoint(ts, quantity.Value()) } - metrics = append(metrics, &metricspb.Metric{ - MetricDescriptor: &metricspb.MetricDescriptor{ - Name: nodeAllocatableMetric, - Description: allocatableDesciption[v1NodeAllocatableTypeValue.String()], - Type: valType, - }, - Timeseries: []*metricspb.TimeSeries{ - val, - }, - }) } + return mb.Emit(imetadata.WithK8sNodeUID(string(node.UID)), imetadata.WithK8sNodeName(node.Name), imetadata.WithOpencensusResourcetype("k8s")) - return []*agentmetricspb.ExportMetricsServiceRequest{ - { - Resource: getResourceForNode(node), - Metrics: metrics, - }, - } -} - -func getNodeConditionMetric(nodeConditionTypeValue string) string { - return fmt.Sprintf("k8s.node.condition_%s", strcase.ToSnake(nodeConditionTypeValue)) -} - -func getNodeAllocatableMetric(nodeAllocatableTypeValue string) string { - return fmt.Sprintf("k8s.node.allocatable_%s", strcase.ToSnake(nodeAllocatableTypeValue)) -} - -func getResourceForNode(node *corev1.Node) *resourcepb.Resource { - return &resourcepb.Resource{ - Type: constants.K8sType, - Labels: map[string]string{ - conventions.AttributeK8SNodeUID: string(node.UID), - conventions.AttributeK8SNodeName: node.Name, - }, - } } var nodeConditionValues = map[corev1.ConditionStatus]int64{ diff --git a/receiver/k8sclusterreceiver/internal/node/nodes_test.go b/receiver/k8sclusterreceiver/internal/node/nodes_test.go index cb75c7f0b340..52432ef0a432 100644 --- a/receiver/k8sclusterreceiver/internal/node/nodes_test.go +++ b/receiver/k8sclusterreceiver/internal/node/nodes_test.go @@ -4,78 +4,34 @@ package node import ( + "path/filepath" "testing" - metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.uber.org/zap" + "go.opentelemetry.io/collector/receiver/receivertest" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/constants" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/golden" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/testutils" ) func TestNodeMetricsReportCPUMetrics(t *testing.T) { n := testutils.NewNode("1") - - actualResourceMetrics := GetMetrics(n, []string{"Ready", "MemoryPressure"}, []string{"cpu", "memory", "ephemeral-storage", "storage"}, zap.NewNop()) - - require.Equal(t, 1, len(actualResourceMetrics)) - - require.Equal(t, 5, len(actualResourceMetrics[0].Metrics)) - testutils.AssertResource(t, actualResourceMetrics[0].Resource, constants.K8sType, - map[string]string{ - "k8s.node.uid": "test-node-1-uid", - "k8s.node.name": "test-node-1", - }, + m := GetMetrics(receivertest.NewNopCreateSettings(), n, []string{"Ready", "MemoryPressure"}, []string{"cpu", "memory", "ephemeral-storage", "storage"}) + 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[0].Metrics[0], "k8s.node.condition_ready", - metricspb.MetricDescriptor_GAUGE_INT64, 1) - - testutils.AssertMetricsInt(t, actualResourceMetrics[0].Metrics[1], "k8s.node.condition_memory_pressure", - metricspb.MetricDescriptor_GAUGE_INT64, 0) - - testutils.AssertMetricsDouble(t, actualResourceMetrics[0].Metrics[2], "k8s.node.allocatable_cpu", - metricspb.MetricDescriptor_GAUGE_DOUBLE, 0.123) - - testutils.AssertMetricsInt(t, actualResourceMetrics[0].Metrics[3], "k8s.node.allocatable_memory", - metricspb.MetricDescriptor_GAUGE_INT64, 456) - - testutils.AssertMetricsInt(t, actualResourceMetrics[0].Metrics[4], "k8s.node.allocatable_ephemeral_storage", - metricspb.MetricDescriptor_GAUGE_INT64, 1234) -} - -func TestGetNodeConditionMetric(t *testing.T) { - tests := []struct { - name string - nodeConditionTypeValue string - want string - }{ - {"Metric for Node condition Ready", - "Ready", - "k8s.node.condition_ready", - }, - {"Metric for Node condition MemoryPressure", - "MemoryPressure", - "k8s.node.condition_memory_pressure", - }, - {"Metric for Node condition DiskPressure", - "DiskPressure", - "k8s.node.condition_disk_pressure", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getNodeConditionMetric(tt.nodeConditionTypeValue); got != tt.want { - t.Errorf("getNodeConditionMetric() = %v, want %v", got, tt.want) - } - }) - } } func TestNodeConditionValue(t *testing.T) { diff --git a/receiver/k8sclusterreceiver/internal/node/testdata/expected.yaml b/receiver/k8sclusterreceiver/internal/node/testdata/expected.yaml new file mode 100644 index 000000000000..7bcc1f327a8e --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/node/testdata/expected.yaml @@ -0,0 +1,48 @@ +resourceMetrics: + - resource: + attributes: + - key: k8s.node.name + value: + stringValue: test-node-1 + - key: k8s.node.uid + value: + stringValue: test-node-1-uid + - key: opencensus.resourcetype + value: + stringValue: k8s + schemaUrl: https://opentelemetry.io/schemas/1.18.0 + scopeMetrics: + - metrics: + - description: Whether this node is Ready (1), not Ready (0) or in an unknown state (-1) + gauge: + dataPoints: + - asInt: "1" + name: k8s.node.condition_ready + unit: "1" + - description: Whether this node is MemoryPressure (1), not MemoryPressure (0) or in an unknown state (-1) + gauge: + dataPoints: + - asInt: "0" + name: k8s.node.condition_memory_pressure + unit: "1" + - description: How many CPU cores remaining that the node can allocate to pods + gauge: + dataPoints: + - asDouble: 0.123 + name: k8s.node.allocatable_cpu + unit: "{cores}" + - description: How many bytes of RAM memory remaining that the node can allocate to pods + gauge: + dataPoints: + - asInt: "456" + name: k8s.node.allocatable_memory + unit: "By" + - description: How many bytes of ephemeral storage remaining that the node can allocate to pods + gauge: + dataPoints: + - asInt: "1234" + name: k8s.node.allocatable_ephemeral_storage + unit: "By" + scope: + name: otelcol/k8sclusterreceiver + version: latest 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..49230f7fd265 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/documentation.md @@ -0,0 +1,31 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# k8s/node + +## 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..fa377dc1a23d --- /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/node 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/node 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/node 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 c675478bd4b2..b7e77869059c 100644 --- a/receiver/k8sclusterreceiver/internal/pod/pods.go +++ b/receiver/k8sclusterreceiver/internal/pod/pods.go @@ -8,9 +8,9 @@ import ( "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" @@ -26,6 +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" + 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" ) @@ -34,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 when using a new pod fields. func Transform(pod *corev1.Pod) *corev1.Pod { @@ -81,75 +76,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, - }, - } - - 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++ + specMetrics := container.GetSpecMetrics(set, c, pod) + specMetrics.ResourceMetrics().MoveAndAppendTo(metrics.ResourceMetrics()) } - 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 { 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..049ec7207694 --- /dev/null +++ b/receiver/k8sclusterreceiver/internal/pod/testdata/expected.yaml @@ -0,0 +1,89 @@ +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 + unit: "1" + 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 + unit: "1" + name: k8s.container.cpu_limit + scope: + name: otelcol/k8sclusterreceiver + version: latest