diff --git a/.chloggen/add_node_uid_k8s_attrib.yaml b/.chloggen/add_node_uid_k8s_attrib.yaml new file mode 100644 index 000000000000..bf178ac5425a --- /dev/null +++ b/.chloggen/add_node_uid_k8s_attrib.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: processor/k8sattributes + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add support for `k8s.node.uid` metadata + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [31637] + +# (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: [] diff --git a/processor/k8sattributesprocessor/README.md b/processor/k8sattributesprocessor/README.md index a2c21b8f9d90..c9cc6b7842e0 100644 --- a/processor/k8sattributesprocessor/README.md +++ b/processor/k8sattributesprocessor/README.md @@ -161,7 +161,7 @@ k8sattributes/2: ## Cluster-scoped RBAC -If you'd like to set up the k8sattributesprocessor to receive telemetry from across namespaces, it will need `get`, `watch` and `list` permissions on both `pods` and `namespaces` resources, for all namespaces and pods included in the configured filters. Additionally, when using `k8s.deployment.uid` or `k8s.deployment.name` the processor also needs `get`, `watch` and `list` permissions for `replicasets` resources. When extracting metadatas from `node`, the processor needs `get`, `watch` and `list` permissions for `nodes` resources. +If you'd like to set up the k8sattributesprocessor to receive telemetry from across namespaces, it will need `get`, `watch` and `list` permissions on both `pods` and `namespaces` resources, for all namespaces and pods included in the configured filters. Additionally, when using `k8s.deployment.uid` or `k8s.deployment.name` the processor also needs `get`, `watch` and `list` permissions for `replicasets` resources. When using `k8s.node.uid` or extracting metadata from `node`, the processor needs `get`, `watch` and `list` permissions for `nodes` resources. Here is an example of a `ClusterRole` to give a `ServiceAccount` the necessary permissions for all pods, nodes, and namespaces in the cluster (replace `` with a namespace where collector is deployed): diff --git a/processor/k8sattributesprocessor/config.go b/processor/k8sattributesprocessor/config.go index 44323b1baebf..809ff4fe301f 100644 --- a/processor/k8sattributesprocessor/config.go +++ b/processor/k8sattributesprocessor/config.go @@ -84,12 +84,17 @@ func (cfg *Config) Validate() error { for _, field := range cfg.Extract.Metadata { switch field { case conventions.AttributeK8SNamespaceName, conventions.AttributeK8SPodName, conventions.AttributeK8SPodUID, - specPodHostName, metadataPodStartTime, conventions.AttributeK8SDeploymentName, conventions.AttributeK8SDeploymentUID, - conventions.AttributeK8SReplicaSetName, conventions.AttributeK8SReplicaSetUID, conventions.AttributeK8SDaemonSetName, - conventions.AttributeK8SDaemonSetUID, conventions.AttributeK8SStatefulSetName, conventions.AttributeK8SStatefulSetUID, - conventions.AttributeK8SContainerName, conventions.AttributeK8SJobName, conventions.AttributeK8SJobUID, - conventions.AttributeK8SCronJobName, conventions.AttributeK8SNodeName, conventions.AttributeContainerID, - conventions.AttributeContainerImageName, conventions.AttributeContainerImageTag, clusterUID: + specPodHostName, metadataPodStartTime, + conventions.AttributeK8SDeploymentName, conventions.AttributeK8SDeploymentUID, + conventions.AttributeK8SReplicaSetName, conventions.AttributeK8SReplicaSetUID, + conventions.AttributeK8SDaemonSetName, conventions.AttributeK8SDaemonSetUID, + conventions.AttributeK8SStatefulSetName, conventions.AttributeK8SStatefulSetUID, + conventions.AttributeK8SJobName, conventions.AttributeK8SJobUID, + conventions.AttributeK8SCronJobName, + conventions.AttributeK8SNodeName, conventions.AttributeK8SNodeUID, + conventions.AttributeK8SContainerName, conventions.AttributeContainerID, + conventions.AttributeContainerImageName, conventions.AttributeContainerImageTag, + clusterUID: default: return fmt.Errorf("\"%s\" is not a supported metadata field", field) } diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index 6364d46a4e4e..91d0f4f648b7 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -171,7 +171,7 @@ func New(logger *zap.Logger, apiCfg k8sconfig.APIConfig, rules ExtractionRules, } } - if c.extractNodeLabelsAnnotations() { + if c.extractNodeLabelsAnnotations() || c.extractNodeUID() { c.nodeInformer = newNodeSharedInformer(c.kc, c.Filters.Node) } @@ -930,6 +930,10 @@ func (c *WatchClient) extractNodeLabelsAnnotations() bool { return false } +func (c *WatchClient) extractNodeUID() bool { + return c.Rules.NodeUID +} + func (c *WatchClient) addOrUpdateNode(node *api_v1.Node) { newNode := &Node{ Name: node.Name, diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index 47e95e98da33..06d025ca21fa 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -210,6 +210,7 @@ type ExtractionRules struct { StatefulSetUID bool StatefulSetName bool Node bool + NodeUID bool StartTime bool ContainerName bool ContainerID bool diff --git a/processor/k8sattributesprocessor/internal/metadata/generated_config.go b/processor/k8sattributesprocessor/internal/metadata/generated_config.go index 28d4bc97a0ce..cda0e14c96ce 100644 --- a/processor/k8sattributesprocessor/internal/metadata/generated_config.go +++ b/processor/k8sattributesprocessor/internal/metadata/generated_config.go @@ -39,6 +39,7 @@ type ResourceAttributesConfig struct { K8sJobUID ResourceAttributeConfig `mapstructure:"k8s.job.uid"` K8sNamespaceName ResourceAttributeConfig `mapstructure:"k8s.namespace.name"` K8sNodeName ResourceAttributeConfig `mapstructure:"k8s.node.name"` + K8sNodeUID ResourceAttributeConfig `mapstructure:"k8s.node.uid"` K8sPodHostname ResourceAttributeConfig `mapstructure:"k8s.pod.hostname"` K8sPodName ResourceAttributeConfig `mapstructure:"k8s.pod.name"` K8sPodStartTime ResourceAttributeConfig `mapstructure:"k8s.pod.start_time"` @@ -93,6 +94,9 @@ func DefaultResourceAttributesConfig() ResourceAttributesConfig { K8sNodeName: ResourceAttributeConfig{ Enabled: true, }, + K8sNodeUID: ResourceAttributeConfig{ + Enabled: false, + }, K8sPodHostname: ResourceAttributeConfig{ Enabled: false, }, diff --git a/processor/k8sattributesprocessor/internal/metadata/generated_config_test.go b/processor/k8sattributesprocessor/internal/metadata/generated_config_test.go index b024a857c8d1..263f271231e9 100644 --- a/processor/k8sattributesprocessor/internal/metadata/generated_config_test.go +++ b/processor/k8sattributesprocessor/internal/metadata/generated_config_test.go @@ -39,6 +39,7 @@ func TestResourceAttributesConfig(t *testing.T) { K8sJobUID: ResourceAttributeConfig{Enabled: true}, K8sNamespaceName: ResourceAttributeConfig{Enabled: true}, K8sNodeName: ResourceAttributeConfig{Enabled: true}, + K8sNodeUID: ResourceAttributeConfig{Enabled: true}, K8sPodHostname: ResourceAttributeConfig{Enabled: true}, K8sPodName: ResourceAttributeConfig{Enabled: true}, K8sPodStartTime: ResourceAttributeConfig{Enabled: true}, @@ -66,6 +67,7 @@ func TestResourceAttributesConfig(t *testing.T) { K8sJobUID: ResourceAttributeConfig{Enabled: false}, K8sNamespaceName: ResourceAttributeConfig{Enabled: false}, K8sNodeName: ResourceAttributeConfig{Enabled: false}, + K8sNodeUID: ResourceAttributeConfig{Enabled: false}, K8sPodHostname: ResourceAttributeConfig{Enabled: false}, K8sPodName: ResourceAttributeConfig{Enabled: false}, K8sPodStartTime: ResourceAttributeConfig{Enabled: false}, diff --git a/processor/k8sattributesprocessor/internal/metadata/generated_resource.go b/processor/k8sattributesprocessor/internal/metadata/generated_resource.go index 2029665c5955..ab0c0746fca4 100644 --- a/processor/k8sattributesprocessor/internal/metadata/generated_resource.go +++ b/processor/k8sattributesprocessor/internal/metadata/generated_resource.go @@ -119,6 +119,13 @@ func (rb *ResourceBuilder) SetK8sNodeName(val string) { } } +// SetK8sNodeUID sets provided value as "k8s.node.uid" attribute. +func (rb *ResourceBuilder) SetK8sNodeUID(val string) { + if rb.config.K8sNodeUID.Enabled { + rb.res.Attributes().PutStr("k8s.node.uid", val) + } +} + // SetK8sPodHostname sets provided value as "k8s.pod.hostname" attribute. func (rb *ResourceBuilder) SetK8sPodHostname(val string) { if rb.config.K8sPodHostname.Enabled { diff --git a/processor/k8sattributesprocessor/internal/metadata/generated_resource_test.go b/processor/k8sattributesprocessor/internal/metadata/generated_resource_test.go index d8ad4098252e..fd96be1e87fe 100644 --- a/processor/k8sattributesprocessor/internal/metadata/generated_resource_test.go +++ b/processor/k8sattributesprocessor/internal/metadata/generated_resource_test.go @@ -27,6 +27,7 @@ func TestResourceBuilder(t *testing.T) { rb.SetK8sJobUID("k8s.job.uid-val") rb.SetK8sNamespaceName("k8s.namespace.name-val") rb.SetK8sNodeName("k8s.node.name-val") + rb.SetK8sNodeUID("k8s.node.uid-val") rb.SetK8sPodHostname("k8s.pod.hostname-val") rb.SetK8sPodName("k8s.pod.name-val") rb.SetK8sPodStartTime("k8s.pod.start_time-val") @@ -43,7 +44,7 @@ func TestResourceBuilder(t *testing.T) { case "default": assert.Equal(t, 8, res.Attributes().Len()) case "all_set": - assert.Equal(t, 22, res.Attributes().Len()) + assert.Equal(t, 23, res.Attributes().Len()) case "none_set": assert.Equal(t, 0, res.Attributes().Len()) return @@ -121,6 +122,11 @@ func TestResourceBuilder(t *testing.T) { if ok { assert.EqualValues(t, "k8s.node.name-val", val.Str()) } + val, ok = res.Attributes().Get("k8s.node.uid") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, "k8s.node.uid-val", val.Str()) + } val, ok = res.Attributes().Get("k8s.pod.hostname") assert.Equal(t, test == "all_set", ok) if ok { diff --git a/processor/k8sattributesprocessor/internal/metadata/testdata/config.yaml b/processor/k8sattributesprocessor/internal/metadata/testdata/config.yaml index 55de9c16c27c..d7f95e5bf943 100644 --- a/processor/k8sattributesprocessor/internal/metadata/testdata/config.yaml +++ b/processor/k8sattributesprocessor/internal/metadata/testdata/config.yaml @@ -29,6 +29,8 @@ all_set: enabled: true k8s.node.name: enabled: true + k8s.node.uid: + enabled: true k8s.pod.hostname: enabled: true k8s.pod.name: @@ -75,6 +77,8 @@ none_set: enabled: false k8s.node.name: enabled: false + k8s.node.uid: + enabled: false k8s.pod.hostname: enabled: false k8s.pod.name: diff --git a/processor/k8sattributesprocessor/metadata.yaml b/processor/k8sattributesprocessor/metadata.yaml index aa481f9c4f65..9ae10a711572 100644 --- a/processor/k8sattributesprocessor/metadata.yaml +++ b/processor/k8sattributesprocessor/metadata.yaml @@ -86,6 +86,10 @@ resource_attributes: description: The name of the Node. type: string enabled: true + k8s.node.uid: + description: The UID of the Node. + type: string + enabled: false container.id: description: Container ID. Usually a UUID, as for example used to identify Docker containers. The UUID might be abbreviated. Requires k8s.container.restart_count. type: string diff --git a/processor/k8sattributesprocessor/options.go b/processor/k8sattributesprocessor/options.go index ab4b4695ab39..033d3bb303c0 100644 --- a/processor/k8sattributesprocessor/options.go +++ b/processor/k8sattributesprocessor/options.go @@ -94,6 +94,9 @@ func enabledAttributes() (attributes []string) { if defaultConfig.K8sNodeName.Enabled { attributes = append(attributes, conventions.AttributeK8SNodeName) } + if defaultConfig.K8sNodeUID.Enabled { + attributes = append(attributes, conventions.AttributeK8SNodeUID) + } if defaultConfig.K8sPodHostname.Enabled { attributes = append(attributes, specPodHostName) } @@ -163,6 +166,8 @@ func withExtractMetadata(fields ...string) option { p.rules.CronJobName = true case conventions.AttributeK8SNodeName: p.rules.Node = true + case conventions.AttributeK8SNodeUID: + p.rules.NodeUID = true case conventions.AttributeContainerID: p.rules.ContainerID = true case conventions.AttributeContainerImageName: diff --git a/processor/k8sattributesprocessor/processor.go b/processor/k8sattributesprocessor/processor.go index 136d224e0a8b..b2c4e82490f0 100644 --- a/processor/k8sattributesprocessor/processor.go +++ b/processor/k8sattributesprocessor/processor.go @@ -166,6 +166,12 @@ func (kp *kubernetesprocessor) processResource(ctx context.Context, resource pco resource.Attributes().PutStr(key, val) } } + nodeUID := kp.getUIDForPodsNode(nodeName) + if nodeUID != "" { + if _, found := resource.Attributes().Get(conventions.AttributeK8SNodeUID); !found { + resource.Attributes().PutStr(conventions.AttributeK8SNodeUID, nodeUID) + } + } } } @@ -263,6 +269,14 @@ func (kp *kubernetesprocessor) getAttributesForPodsNode(nodeName string) map[str return node.Attributes } +func (kp *kubernetesprocessor) getUIDForPodsNode(nodeName string) string { + node, ok := kp.kc.GetNode(nodeName) + if !ok { + return "" + } + return node.NodeUID +} + // intFromAttribute extracts int value from an attribute stored as string or int func intFromAttribute(val pcommon.Value) (int, error) { switch val.Type() { diff --git a/processor/k8sattributesprocessor/processor_test.go b/processor/k8sattributesprocessor/processor_test.go index 94d6ee91d7fd..d22de00d7259 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -880,6 +880,72 @@ func TestAddNodeLabels(t *testing.T) { }) } +func TestAddNodeUID(t *testing.T) { + nodeUID := "asdfasdf-asdfasdf-asdf" + m := newMultiTest( + t, + func() component.Config { + cfg := createDefaultConfig().(*Config) + cfg.Extract.Metadata = []string{"k8s.node.uid"} + cfg.Extract.Labels = []FieldExtractConfig{} + return cfg + }(), + nil, + ) + + podIP := "1.1.1.1" + nodes := map[string]map[string]string{ + "node-1": { + "nodelabel": "1", + }, + } + m.kubernetesProcessorOperation(func(kp *kubernetesprocessor) { + kp.podAssociations = []kube.Association{ + { + Sources: []kube.AssociationSource{ + { + From: "connection", + }, + }, + }, + } + }) + + m.kubernetesProcessorOperation(func(kp *kubernetesprocessor) { + pi := kube.PodIdentifier{ + kube.PodIdentifierAttributeFromConnection(podIP), + } + kp.kc.(*fakeClient).Pods[pi] = &kube.Pod{Name: "test-2323", NodeName: "node-1"} + kp.kc.(*fakeClient).Nodes = make(map[string]*kube.Node) + for ns, labels := range nodes { + kp.kc.(*fakeClient).Nodes[ns] = &kube.Node{Attributes: labels, NodeUID: nodeUID} + } + }) + + ctx := client.NewContext(context.Background(), client.Info{ + Addr: &net.IPAddr{ + IP: net.ParseIP(podIP), + }, + }) + m.testConsume( + ctx, + generateTraces(), + generateMetrics(), + generateLogs(), + func(err error) { + assert.NoError(t, err) + }) + + m.assertBatchesLen(1) + m.assertResourceObjectLen(0) + m.assertResource(0, func(res pcommon.Resource) { + assert.Equal(t, 3, res.Attributes().Len()) + assertResourceHasStringAttribute(t, res, "k8s.pod.ip", podIP) + assertResourceHasStringAttribute(t, res, "k8s.node.uid", nodeUID) + assertResourceHasStringAttribute(t, res, "nodelabel", "1") + }) +} + func TestProcessorAddContainerAttributes(t *testing.T) { tests := []struct { name string