diff --git a/.chloggen/partial-fix-issue-39038.yaml b/.chloggen/partial-fix-issue-39038.yaml new file mode 100644 index 0000000000000..9acbd1867b5fc --- /dev/null +++ b/.chloggen/partial-fix-issue-39038.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: k8sclusterreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add missing attributes to entities in experimental entity feature + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [39038] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/receiver/k8sclusterreceiver/internal/constants/constants.go b/receiver/k8sclusterreceiver/internal/constants/constants.go index 191a51d365cad..0ed4c3838df03 100644 --- a/receiver/k8sclusterreceiver/internal/constants/constants.go +++ b/receiver/k8sclusterreceiver/internal/constants/constants.go @@ -14,11 +14,15 @@ const ( K8sKeyReplicationControllerUID = "k8s.replicationcontroller.uid" K8sKeyResourceQuotaUID = "k8s.resourcequota.uid" K8sKeyClusterResourceQuotaUID = "openshift.clusterquota.uid" + K8sKeyPodUID = "k8s.pod.uid" // Resource labels keys for Name. K8sKeyReplicationControllerName = "k8s.replicationcontroller.name" K8sKeyResourceQuotaName = "k8s.resourcequota.name" K8sKeyClusterResourceQuotaName = "openshift.clusterquota.name" + K8sKeyNamespaceName = "k8s.namespace.name" + K8sKeyPodName = "k8s.pod.name" + K8sKeyNodeName = "k8s.node.name" // Kubernetes resource kinds K8sKindCronJob = "CronJob" diff --git a/receiver/k8sclusterreceiver/internal/container/containers.go b/receiver/k8sclusterreceiver/internal/container/containers.go index 78253981a60bc..e7668b37c9e2b 100644 --- a/receiver/k8sclusterreceiver/internal/container/containers.go +++ b/receiver/k8sclusterreceiver/internal/container/containers.go @@ -13,6 +13,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/docker" metadataPkg "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" imetadata "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver/internal/utils" @@ -23,6 +24,9 @@ const ( containerKeyStatus = "container.status" containerKeyStatusReason = "container.status.reason" containerCreationTimestamp = "container.creation_timestamp" + containerName = "k8s.container.name" + containerImageName = "container.image.name" + containerImageTag = "container.image.tag" // Values for container metadata containerStatusRunning = "running" @@ -96,9 +100,23 @@ func RecordSpecMetrics(logger *zap.Logger, mb *imetadata.MetricsBuilder, c corev mb.EmitForResource(imetadata.WithResource(rb.Emit())) } -func GetMetadata(cs corev1.ContainerStatus) *metadata.KubernetesMetadata { +func GetMetadata(pod *corev1.Pod, cs corev1.ContainerStatus, logger *zap.Logger) *metadata.KubernetesMetadata { mdata := map[string]string{} + imageStr := cs.Image + image, err := docker.ParseImageName(cs.Image) + if err != nil { + docker.LogParseError(err, imageStr, logger) + } else { + mdata[containerImageName] = image.Repository + mdata[containerImageTag] = image.Tag + } + mdata[containerName] = cs.Name + mdata[constants.K8sKeyPodName] = pod.Name + mdata[constants.K8sKeyPodUID] = string(pod.UID) + mdata[constants.K8sKeyNamespaceName] = pod.Namespace + mdata[constants.K8sKeyNodeName] = pod.Spec.NodeName + if cs.State.Running != nil { mdata[containerKeyStatus] = containerStatusRunning if !cs.State.Running.StartedAt.IsZero() { diff --git a/receiver/k8sclusterreceiver/internal/container/containers_test.go b/receiver/k8sclusterreceiver/internal/container/containers_test.go index 030aca08b2fd1..d2f6ccd2f3c23 100644 --- a/receiver/k8sclusterreceiver/internal/container/containers_test.go +++ b/receiver/k8sclusterreceiver/internal/container/containers_test.go @@ -9,18 +9,42 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/zap" 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" ) func TestGetMetadata(t *testing.T) { refTime := v1.Now() + pod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + UID: types.UID("test-pod-uid"), + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + }, + } + tests := []struct { - name string - containerState corev1.ContainerState - expectedStatus string - expectedReason string - expectedStartedAt string + name string + containerState corev1.ContainerState + expectedStatus string + expectedReason string + expectedStartedAt string + containerName string + containerID string + containerImage string + containerImageName string + containerImageTag string + podName string + podUID string + nodeName string + namespaceName string }{ { name: "Running container", @@ -29,8 +53,17 @@ func TestGetMetadata(t *testing.T) { StartedAt: refTime, }, }, - expectedStatus: containerStatusRunning, - expectedStartedAt: refTime.Format(time.RFC3339), + expectedStatus: containerStatusRunning, + expectedStartedAt: refTime.Format(time.RFC3339), + containerName: "my-test-container1", + containerID: "f37ee861-f093-4cea-aa26-f39fff8b0998", + containerImage: "docker/someimage1:v1.0", + containerImageName: "docker/someimage1", + containerImageTag: "v1.0", + podName: pod.Name, + podUID: string(pod.UID), + namespaceName: "test-namespace", + nodeName: "test-node", }, { name: "Terminated container", @@ -43,9 +76,18 @@ func TestGetMetadata(t *testing.T) { ExitCode: 0, }, }, - expectedStatus: containerStatusTerminated, - expectedReason: "Completed", - expectedStartedAt: refTime.Format(time.RFC3339), + expectedStatus: containerStatusTerminated, + expectedReason: "Completed", + expectedStartedAt: refTime.Format(time.RFC3339), + containerName: "my-test-container2", + containerID: "f37ee861-f093-4cea-aa26-f39fff8b0997", + containerImage: "docker/someimage2:v1.1", + containerImageName: "docker/someimage2", + containerImageTag: "v1.1", + podName: pod.Name, + podUID: string(pod.UID), + namespaceName: "test-namespace", + nodeName: "test-node", }, { name: "Waiting container", @@ -54,17 +96,29 @@ func TestGetMetadata(t *testing.T) { Reason: "CrashLoopBackOff", }, }, - expectedStatus: containerStatusWaiting, - expectedReason: "CrashLoopBackOff", + expectedStatus: containerStatusWaiting, + expectedReason: "CrashLoopBackOff", + containerName: "my-test-container3", + containerID: "f37ee861-f093-4cea-aa26-f39fff8b0996", + containerImage: "docker/someimage3:latest", + containerImageName: "docker/someimage3", + containerImageTag: "latest", + podName: pod.Name, + podUID: string(pod.UID), + namespaceName: "test-namespace", + nodeName: "test-node", }, } - + logger := zap.NewNop() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cs := corev1.ContainerStatus{ - State: tt.containerState, + State: tt.containerState, + Name: tt.containerName, + ContainerID: tt.containerID, + Image: tt.containerImage, } - md := GetMetadata(cs) + md := GetMetadata(pod, cs, logger) require.NotNil(t, md) assert.Equal(t, tt.expectedStatus, md.Metadata[containerKeyStatus]) @@ -75,6 +129,13 @@ func TestGetMetadata(t *testing.T) { assert.Contains(t, md.Metadata, containerCreationTimestamp) assert.Equal(t, tt.expectedStartedAt, md.Metadata[containerCreationTimestamp]) } + assert.Equal(t, tt.containerName, md.Metadata[containerName]) + assert.Equal(t, tt.containerImageName, md.Metadata[containerImageName]) + assert.Equal(t, tt.containerImageTag, md.Metadata[containerImageTag]) + assert.Equal(t, tt.podName, md.Metadata[constants.K8sKeyPodName]) + assert.Equal(t, tt.podUID, md.Metadata[constants.K8sKeyPodUID]) + assert.Equal(t, tt.namespaceName, md.Metadata[constants.K8sKeyNamespaceName]) + assert.Equal(t, tt.nodeName, md.Metadata[constants.K8sKeyNodeName]) }) } } diff --git a/receiver/k8sclusterreceiver/internal/cronjob/cronjobs_test.go b/receiver/k8sclusterreceiver/internal/cronjob/cronjobs_test.go index d89cfa5843149..1e79c32fb1223 100644 --- a/receiver/k8sclusterreceiver/internal/cronjob/cronjobs_test.go +++ b/receiver/k8sclusterreceiver/internal/cronjob/cronjobs_test.go @@ -59,6 +59,7 @@ func TestCronJobMetadata(t *testing.T) { "concurrency_policy": "concurrency_policy", "k8s.workload.kind": "CronJob", "k8s.workload.name": "test-cronjob-1", + "k8s.namespace.name": "test-namespace", }, }, *actualMetadata["test-cronjob-1-uid"], diff --git a/receiver/k8sclusterreceiver/internal/metadata/metadata.go b/receiver/k8sclusterreceiver/internal/metadata/metadata.go index 5ea50a953f7b5..8f5442b7bd6c8 100644 --- a/receiver/k8sclusterreceiver/internal/metadata/metadata.go +++ b/receiver/k8sclusterreceiver/internal/metadata/metadata.go @@ -55,6 +55,7 @@ func GetGenericMetadata(om *v1.ObjectMeta, resourceType string) *KubernetesMetad metadata[constants.K8sKeyWorkLoadKind] = resourceType metadata[constants.K8sKeyWorkLoadName] = om.Name metadata[rType+".creation_timestamp"] = om.GetCreationTimestamp().Format(time.RFC3339) + metadata[constants.K8sKeyNamespaceName] = om.Namespace for _, or := range om.OwnerReferences { kind := strings.ToLower(or.Kind) diff --git a/receiver/k8sclusterreceiver/internal/metadata/metadata_test.go b/receiver/k8sclusterreceiver/internal/metadata/metadata_test.go index 8f7015a12ac07..26a49ebc0645e 100644 --- a/receiver/k8sclusterreceiver/internal/metadata/metadata_test.go +++ b/receiver/k8sclusterreceiver/internal/metadata/metadata_test.go @@ -19,6 +19,7 @@ func Test_getGenericMetadata(t *testing.T) { om := &v1.ObjectMeta{ Name: "test-name", UID: "test-uid", + Namespace: "test-namespace", Generation: 0, CreationTimestamp: v1.NewTime(now), Labels: map[string]string{ @@ -46,6 +47,7 @@ func Test_getGenericMetadata(t *testing.T) { assert.Equal(t, map[string]string{ "k8s.workload.name": "test-name", "k8s.workload.kind": "ResourceType", + "k8s.namespace.name": "test-namespace", "resourcetype.creation_timestamp": now.Format(time.RFC3339), "k8s.owner-kind-1.name": "owner1", "k8s.owner-kind-1.uid": "owner1", diff --git a/receiver/k8sclusterreceiver/internal/pod/pods.go b/receiver/k8sclusterreceiver/internal/pod/pods.go index 263e4c3ac5f0c..af38f5f26a60d 100644 --- a/receiver/k8sclusterreceiver/internal/pod/pods.go +++ b/receiver/k8sclusterreceiver/internal/pod/pods.go @@ -164,6 +164,9 @@ func GetMetadata(pod *corev1.Pod, mc *metadata.Store, logger *zap.Logger) map[ex meta = maps.MergeStringMaps(meta, collectPodReplicaSetProperties(pod, store, logger)) } + meta[constants.K8sKeyNamespaceName] = pod.Namespace + meta[constants.K8sKeyPodName] = pod.Name + podID := experimentalmetricmetadata.ResourceID(pod.UID) return metadata.MergeKubernetesMetadataMaps(map[experimentalmetricmetadata.ResourceID]*metadata.KubernetesMetadata{ podID: { @@ -172,7 +175,7 @@ func GetMetadata(pod *corev1.Pod, mc *metadata.Store, logger *zap.Logger) map[ex ResourceID: podID, Metadata: meta, }, - }, getPodContainerProperties(pod)) + }, getPodContainerProperties(pod, logger)) } // collectPodJobProperties checks if pod owner of type Job is cached. Check owners reference @@ -249,10 +252,10 @@ func getWorkloadProperties(ref *v1.OwnerReference, labelKey string) map[string]s } } -func getPodContainerProperties(pod *corev1.Pod) map[experimentalmetricmetadata.ResourceID]*metadata.KubernetesMetadata { +func getPodContainerProperties(pod *corev1.Pod, logger *zap.Logger) map[experimentalmetricmetadata.ResourceID]*metadata.KubernetesMetadata { km := map[experimentalmetricmetadata.ResourceID]*metadata.KubernetesMetadata{} for _, cs := range pod.Status.ContainerStatuses { - md := container.GetMetadata(cs) + md := container.GetMetadata(pod, cs, logger) km[md.ResourceID] = md } return km diff --git a/receiver/k8sclusterreceiver/internal/pod/pods_test.go b/receiver/k8sclusterreceiver/internal/pod/pods_test.go index 4013207fdf8af..2e1f456733735 100644 --- a/receiver/k8sclusterreceiver/internal/pod/pods_test.go +++ b/receiver/k8sclusterreceiver/internal/pod/pods_test.go @@ -235,8 +235,10 @@ func testCaseForPodWorkload(to testCaseOptions) testCase { func expectedKubernetesMetadata(to testCaseOptions) map[experimentalmetricmetadata.ResourceID]*metadata.KubernetesMetadata { podUIDLabel := "test-pod-0-uid" + podNameLabel := "test-pod-0" kindLower := strings.ToLower(to.kind) kindObjName := fmt.Sprintf("test-%s-0", kindLower) + namespaceLabel := "test-namespace" kindObjUID := fmt.Sprintf("test-%s-0-uid", kindLower) kindNameLabel := fmt.Sprintf("k8s.%s.name", kindLower) kindUIDLabel := fmt.Sprintf("k8s.%s.uid", kindLower) @@ -247,9 +249,11 @@ func expectedKubernetesMetadata(to testCaseOptions) map[experimentalmetricmetada ResourceIDKey: "k8s.pod.uid", ResourceID: experimentalmetricmetadata.ResourceID(podUIDLabel), Metadata: map[string]string{ - kindNameLabel: kindObjName, - kindUIDLabel: kindObjUID, - "k8s.pod.phase": "Unknown", // Default value when phase is not set. + kindNameLabel: kindObjName, + kindUIDLabel: kindObjUID, + "k8s.pod.phase": "Unknown", // Default value when phase is not set. + "k8s.namespace.name": namespaceLabel, + "k8s.pod.name": podNameLabel, }, }, } @@ -485,6 +489,8 @@ func TestTransform(t *testing.T) { func TestPodMetadata(t *testing.T) { tests := []struct { name string + podName string + namespace string statusPhase corev1.PodPhase statusReason string expectedMetadata map[string]string @@ -494,6 +500,8 @@ func TestPodMetadata(t *testing.T) { statusPhase: corev1.PodFailed, statusReason: "Evicted", expectedMetadata: map[string]string{ + "k8s.pod.name": "test-pod-0", + "k8s.namespace.name": "test-namespace", "k8s.pod.phase": "Failed", "k8s.pod.status_reason": "Evicted", "k8s.workload.kind": "Deployment", @@ -509,6 +517,8 @@ func TestPodMetadata(t *testing.T) { statusPhase: corev1.PodRunning, statusReason: "", expectedMetadata: map[string]string{ + "k8s.pod.name": "test-pod-0", + "k8s.namespace.name": "test-namespace", "k8s.pod.phase": "Running", "k8s.workload.kind": "Deployment", "k8s.workload.name": "test-deployment-0", diff --git a/receiver/k8sclusterreceiver/internal/statefulset/statefulsets_test.go b/receiver/k8sclusterreceiver/internal/statefulset/statefulsets_test.go index 5cdc19fc47768..4f2aaaaecbfbe 100644 --- a/receiver/k8sclusterreceiver/internal/statefulset/statefulsets_test.go +++ b/receiver/k8sclusterreceiver/internal/statefulset/statefulsets_test.go @@ -66,6 +66,7 @@ func TestStatefulsetMetadata(t *testing.T) { Metadata: map[string]string{ "k8s.workload.name": "test-statefulset-1", "k8s.workload.kind": "StatefulSet", + "k8s.namespace.name": "test-namespace", "statefulset.creation_timestamp": "0001-01-01T00:00:00Z", "foo": "bar", "foo1": "", diff --git a/receiver/k8sclusterreceiver/watcher_test.go b/receiver/k8sclusterreceiver/watcher_test.go index 308c00158a12a..104f0176298a6 100644 --- a/receiver/k8sclusterreceiver/watcher_test.go +++ b/receiver/k8sclusterreceiver/watcher_test.go @@ -270,7 +270,7 @@ func TestSyncMetadataAndEmitEntityEvents(t *testing.T) { "otel.entity.interval": int64(7200000), // 2h in milliseconds "otel.entity.type": "k8s.pod", "otel.entity.id": map[string]any{"k8s.pod.uid": "pod0"}, - "otel.entity.attributes": map[string]any{"pod.creation_timestamp": "0001-01-01T00:00:00Z", "k8s.pod.phase": "Unknown"}, + "otel.entity.attributes": map[string]any{"pod.creation_timestamp": "0001-01-01T00:00:00Z", "k8s.pod.phase": "Unknown", "k8s.namespace.name": "test", "k8s.pod.name": "0"}, } assert.EqualValues(t, expected, lr.Attributes().AsRaw()) assert.WithinRange(t, lr.Timestamp().AsTime(), step1, step2) @@ -324,7 +324,7 @@ func TestObjMetadata(t *testing.T) { EntityType: "k8s.pod", ResourceIDKey: "k8s.pod.uid", ResourceID: "test-pod-0-uid", - Metadata: allPodMetadata(map[string]string{"k8s.pod.phase": "Succeeded"}), + Metadata: allPodMetadata(map[string]string{"k8s.pod.phase": "Succeeded", "k8s.pod.name": "test-pod-0", "k8s.namespace.name": "test-namespace"}), }, experimentalmetricmetadata.ResourceID("container-id"): { EntityType: "container", @@ -333,6 +333,13 @@ func TestObjMetadata(t *testing.T) { Metadata: map[string]string{ "container.status": "running", "container.creation_timestamp": "0001-01-01T01:01:01Z", + "container.image.name": "container-image-name", + "container.image.tag": "latest", + "k8s.container.name": "container-name", + "k8s.pod.name": "test-pod-0", + "k8s.pod.uid": "test-pod-0-uid", + "k8s.namespace.name": "test-namespace", + "k8s.node.name": "test-node", }, }, }, @@ -359,6 +366,8 @@ func TestObjMetadata(t *testing.T) { "k8s.statefulset.uid": "test-statefulset-0-uid", "k8s.pod.phase": "Failed", "k8s.pod.status_reason": "Evicted", + "k8s.pod.name": "test-pod-0", + "k8s.namespace.name": "test-namespace", }), }, }, @@ -398,6 +407,8 @@ func TestObjMetadata(t *testing.T) { "k8s.service.test-service": "", "k8s-app": "my-app", "k8s.pod.phase": "Running", + "k8s.namespace.name": "test-namespace", + "k8s.pod.name": "test-pod-0", }), }, }, @@ -415,6 +426,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.kind": "DaemonSet", "k8s.workload.name": "test-daemonset-1", "daemonset.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, }, @@ -433,6 +445,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.name": "test-deployment-1", "k8s.deployment.name": "test-deployment-1", "deployment.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, }, @@ -450,6 +463,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.kind": "HPA", "k8s.workload.name": "test-hpa-1", "hpa.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, }, @@ -469,6 +483,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.kind": "Job", "k8s.workload.name": "test-job-1", "job.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, }, @@ -511,6 +526,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.kind": "ReplicaSet", "k8s.workload.name": "test-replicaset-1", "replicaset.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, }, @@ -534,6 +550,7 @@ func TestObjMetadata(t *testing.T) { "k8s.workload.kind": "ReplicationController", "k8s.workload.name": "test-replicationcontroller-1", "replicationcontroller.creation_timestamp": "0001-01-01T00:00:00Z", + "k8s.namespace.name": "test-namespace", }, }, },