diff --git a/.chloggen/operator-resource-attributes.yaml b/.chloggen/operator-resource-attributes.yaml new file mode 100644 index 000000000000..789d52cc5cbf --- /dev/null +++ b/.chloggen/operator-resource-attributes.yaml @@ -0,0 +1,13 @@ +change_type: enhancement + +component: k8sattributesprocessor + +note: Add option to configure resource attributes using the same logic as the OTel operator + +issues: [37114] + +subtext: | + If you are using the file log receiver, you can now create the same resource attributes as traces (via OTLP) received + from an application instrumented with the OpenTelemetry Operator - + simply by adding the `extract: { operator_rules: { enabled: true } }` configuration to the `k8sattributesprocessor` processor. + See the [documentation](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md#config-example) for more details. diff --git a/processor/k8sattributesprocessor/README.md b/processor/k8sattributesprocessor/README.md index ca38d8599ff9..733df48bf8a5 100644 --- a/processor/k8sattributesprocessor/README.md +++ b/processor/k8sattributesprocessor/README.md @@ -272,10 +272,19 @@ k8sattributes/2: - k8s.node.name - k8s.pod.start_time labels: - # This label extraction rule takes the value 'app.kubernetes.io/component' label and maps it to the 'app.label.component' attribute which will be added to the associated resources - - tag_name: app.label.component - key: app.kubernetes.io/component - from: pod + # This label extraction rule takes the value 'app.kubernetes.io/component' label and maps it to the 'app.label.component' attribute which will be added to the associated resources + - tag_name: app.label.component + key: app.kubernetes.io/component + from: pod + operator_rules: + # Apply the operator rules - see https://github.com/open-telemetry/opentelemetry-operator#configure-resource-attributes + enabled: true + # Also translate the following labels to the specified resource attributes: + # app.kubernetes.io/name => service.name + # app.kubernetes.io/version => service.version + # app.kubernetes.io/part-of => service.namespace + # This setting is ignored if 'enabled' is set to false + labels: true pod_association: - sources: # This rule associates all resources containing the 'k8s.pod.ip' attribute with the matching pods. If this attribute is not present in the resource, this rule will not be able to find the matching pod. diff --git a/processor/k8sattributesprocessor/config.go b/processor/k8sattributesprocessor/config.go index 336a50cd3809..e1306e53f9a1 100644 --- a/processor/k8sattributesprocessor/config.go +++ b/processor/k8sattributesprocessor/config.go @@ -178,6 +178,8 @@ type ExtractConfig struct { // It is a list of FieldExtractConfig type. See FieldExtractConfig // documentation for more details. Labels []FieldExtractConfig `mapstructure:"labels"` + + OperatorRules kube.OperatorRules `mapstructure:"operator_rules"` } // FieldExtractConfig allows specifying an extraction rule to extract a resource attribute from pod (or namespace) diff --git a/processor/k8sattributesprocessor/factory.go b/processor/k8sattributesprocessor/factory.go index c3b66d049cac..45befce69c02 100644 --- a/processor/k8sattributesprocessor/factory.go +++ b/processor/k8sattributesprocessor/factory.go @@ -192,6 +192,7 @@ func createProcessorOpts(cfg component.Config) []option { opts = append(opts, withExtractMetadata(oCfg.Extract.Metadata...)) opts = append(opts, withExtractLabels(oCfg.Extract.Labels...)) opts = append(opts, withExtractAnnotations(oCfg.Extract.Annotations...)) + opts = append(opts, withOperatorExtractRules(oCfg.Extract.OperatorRules)) // filters opts = append(opts, withFilterNode(oCfg.Filter.Node, oCfg.Filter.NodeFromEnvVar)) diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index a984e7f85cbd..df84c94d3970 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -451,11 +451,15 @@ func (c *WatchClient) GetNode(nodeName string) (*Node, bool) { return nil, false } -func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { +func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) (map[string]string, map[string]string) { tags := map[string]string{} + serviceNames := map[string]string{} if c.Rules.PodName { tags[conventions.AttributeK8SPodName] = pod.Name } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SPodName] = pod.Name + } if c.Rules.PodHostName { tags[tagHostName] = pod.Spec.Hostname @@ -494,7 +498,7 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { c.Rules.JobUID || c.Rules.JobName || c.Rules.StatefulSetUID || c.Rules.StatefulSetName || c.Rules.DeploymentName || c.Rules.DeploymentUID || - c.Rules.CronJobName { + c.Rules.CronJobName || c.Rules.OperatorRules.Enabled { for _, ref := range pod.OwnerReferences { switch ref.Kind { case "ReplicaSet": @@ -504,11 +508,19 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { if c.Rules.ReplicaSetName { tags[conventions.AttributeK8SReplicaSetName] = ref.Name } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SReplicaSetName] = ref.Name + } if c.Rules.DeploymentName { - if replicaset, ok := c.getReplicaSet(string(ref.UID)); ok { - if replicaset.Deployment.Name != "" { - tags[conventions.AttributeK8SDeploymentName] = replicaset.Deployment.Name - } + name := c.deploymentName(ref) + if name != "" { + tags[conventions.AttributeK8SDeploymentName] = name + } + } + if c.Rules.OperatorRules.Enabled { + name := c.deploymentName(ref) + if name != "" { + serviceNames[conventions.AttributeK8SDeploymentName] = name } } if c.Rules.DeploymentUID { @@ -525,6 +537,9 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { if c.Rules.DaemonSetName { tags[conventions.AttributeK8SDaemonSetName] = ref.Name } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SDaemonSetName] = ref.Name + } case "StatefulSet": if c.Rules.StatefulSetUID { tags[conventions.AttributeK8SStatefulSetUID] = string(ref.UID) @@ -532,11 +547,20 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { if c.Rules.StatefulSetName { tags[conventions.AttributeK8SStatefulSetName] = ref.Name } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SStatefulSetName] = ref.Name + } case "Job": - if c.Rules.CronJobName { + if c.Rules.CronJobName || c.Rules.OperatorRules.Enabled { parts := c.cronJobRegex.FindStringSubmatch(ref.Name) if len(parts) == 2 { - tags[conventions.AttributeK8SCronJobName] = parts[1] + name := parts[1] + if c.Rules.CronJobName { + tags[conventions.AttributeK8SCronJobName] = name + } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SCronJobName] = name + } } } if c.Rules.JobUID { @@ -545,6 +569,9 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { if c.Rules.JobName { tags[conventions.AttributeK8SJobName] = ref.Name } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SJobName] = ref.Name + } } } } @@ -568,7 +595,16 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { for _, r := range c.Rules.Annotations { r.extractFromPodMetadata(pod.Annotations, tags, "k8s.pod.annotations.%s") } - return tags + return tags, serviceNames +} + +func (c *WatchClient) deploymentName(ref meta_v1.OwnerReference) string { + if replicaset, ok := c.getReplicaSet(string(ref.UID)); ok { + if replicaset.Deployment.Name != "" { + return replicaset.Deployment.Name + } + } + return "" } // This function removes all data from the Pod except what is required by extraction rules and pod association @@ -633,7 +669,7 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po removeUnnecessaryContainerData := func(c api_v1.Container) api_v1.Container { transformedContainer := api_v1.Container{} transformedContainer.Name = c.Name // we always need the name, it's used for identification - if rules.ContainerImageName || rules.ContainerImageTag { + if rules.ContainerImageName || rules.ContainerImageTag || rules.OperatorRules.Enabled { transformedContainer.Image = c.Image } return transformedContainer @@ -698,7 +734,7 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain if !needContainerAttributes(c.Rules) { return containers } - if c.Rules.ContainerImageName || c.Rules.ContainerImageTag { + if c.Rules.ContainerImageName || c.Rules.ContainerImageTag || c.Rules.OperatorRules.Enabled { for _, spec := range append(pod.Spec.Containers, pod.Spec.InitContainers...) { container := &Container{} name, tag, err := parseNameAndTagFromImage(spec.Image) @@ -709,18 +745,26 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain if c.Rules.ContainerImageTag { container.ImageTag = tag } + if c.Rules.OperatorRules.Enabled { + container.ServiceVersion = tag + } } containers.ByName[spec.Name] = container } } for _, apiStatus := range append(pod.Status.ContainerStatuses, pod.Status.InitContainerStatuses...) { - container, ok := containers.ByName[apiStatus.Name] + containerName := apiStatus.Name + container, ok := containers.ByName[containerName] if !ok { container = &Container{} - containers.ByName[apiStatus.Name] = container + containers.ByName[containerName] = container } if c.Rules.ContainerName { - container.Name = apiStatus.Name + container.Name = containerName + } + if c.Rules.OperatorRules.Enabled { + container.ServiceInstanceID = operatorServiceInstanceID(pod, containerName) + container.ServiceName = containerName } containerID := apiStatus.ContainerID // Remove container runtime prefix @@ -796,7 +840,7 @@ func (c *WatchClient) podFromAPI(pod *api_v1.Pod) *Pod { if c.shouldIgnorePod(pod) { newPod.Ignore = true } else { - newPod.Attributes = c.extractPodAttributes(pod) + newPod.Attributes, newPod.ServiceNames = c.extractPodAttributes(pod) if needContainerAttributes(c.Rules) { newPod.Containers = c.extractPodContainersAttributes(pod) } @@ -1040,7 +1084,8 @@ func needContainerAttributes(rules ExtractionRules) bool { rules.ContainerName || rules.ContainerImageTag || rules.ContainerImageRepoDigests || - rules.ContainerID + rules.ContainerID || + rules.OperatorRules.Enabled } func (c *WatchClient) handleReplicaSetAdd(obj any) { diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index de701f6fd673..fb7faf9af092 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -702,15 +702,27 @@ func TestExtractionRules(t *testing.T) { }, } + operatorRules := ExtractionRules{ + OperatorRules: OperatorRules{ + Enabled: true, + Labels: true, + }, + Annotations: []FieldExtractionRule{OperatorAnnotationRule}, + Labels: OperatorLabelRules, + } + testCases := []struct { - name string - rules ExtractionRules - attributes map[string]string + name string + rules ExtractionRules + additionalAnnotations map[string]string + additionalLabels map[string]string + attributes map[string]string + serviceName string }{ { name: "no-rules", rules: ExtractionRules{}, - attributes: nil, + attributes: map[string]string{}, }, { name: "deployment", @@ -962,6 +974,44 @@ func TestExtractionRules(t *testing.T) { "prefix-annotation1": "av1", }, }, + { + name: "operator-rules-builtin", + rules: operatorRules, + attributes: map[string]string{ + // tested in operator-container-level-attributes below + }, + serviceName: "auth-service", + }, + { + name: "operator-rules-label-values", + rules: operatorRules, + additionalLabels: map[string]string{ + "app.kubernetes.io/name": "label-service", + "app.kubernetes.io/version": "label-version", + "app.kubernetes.io/part-of": "label-namespace", + }, + attributes: map[string]string{ + "service.name": "label-service", + "service.version": "label-version", + "service.namespace": "label-namespace", + }, + }, + { + name: "operator-rules-annotation-override", + rules: operatorRules, + additionalAnnotations: map[string]string{ + "resource.opentelemetry.io/service.instance.id": "annotation-id", + "resource.opentelemetry.io/service.version": "annotation-version", + "resource.opentelemetry.io/service.name": "annotation-service", + "resource.opentelemetry.io/service.namespace": "annotation-namespace", + }, + attributes: map[string]string{ + "service.instance.id": "annotation-id", + "service.name": "annotation-service", + "service.version": "annotation-version", + "service.namespace": "annotation-namespace", + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -969,18 +1019,23 @@ func TestExtractionRules(t *testing.T) { // manually call the data removal functions here // normally the informer does this, but fully emulating the informer in this test is annoying - transformedPod := removeUnnecessaryPodData(pod, c.Rules) + podCopy := pod.DeepCopy() + for k, v := range tc.additionalAnnotations { + podCopy.Annotations[k] = v + } + for k, v := range tc.additionalLabels { + podCopy.Labels[k] = v + } + transformedPod := removeUnnecessaryPodData(podCopy, c.Rules) transformedReplicaset := removeUnnecessaryReplicaSetData(replicaset) c.handleReplicaSetAdd(transformedReplicaset) c.handlePodAdd(transformedPod) - p, ok := c.GetPod(newPodIdentifier("connection", "", pod.Status.PodIP)) + p, ok := c.GetPod(newPodIdentifier("connection", "", podCopy.Status.PodIP)) require.True(t, ok) - assert.Equal(t, len(tc.attributes), len(p.Attributes)) - for k, v := range tc.attributes { - got, ok := p.Attributes[k] - assert.True(t, ok) - assert.Equal(t, v, got) + assert.Equal(t, tc.attributes, p.Attributes) + if tc.serviceName != "" { + assert.Equal(t, tc.serviceName, OperatorServiceName("containerName", p.ServiceNames)) } }) } @@ -1040,7 +1095,7 @@ func TestReplicaSetExtractionRules(t *testing.T) { { name: "no-rules", rules: ExtractionRules{}, - attributes: nil, + attributes: map[string]string{}, }, { name: "one_deployment_is_controller", ownerReferences: []meta_v1.OwnerReference{ @@ -1132,12 +1187,7 @@ func TestReplicaSetExtractionRules(t *testing.T) { p, ok := c.GetPod(newPodIdentifier("connection", "", pod.Status.PodIP)) require.True(t, ok) - assert.Equal(t, len(tc.attributes), len(p.Attributes)) - for k, v := range tc.attributes { - got, ok := p.Attributes[k] - assert.True(t, ok) - assert.Equal(t, v, got) - } + assert.Equal(t, tc.attributes, p.Attributes) }) } } @@ -1489,6 +1539,10 @@ func TestPodIgnorePatterns(t *testing.T) { func Test_extractPodContainersAttributes(t *testing.T) { pod := api_v1.Pod{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + }, Spec: api_v1.PodSpec{ Containers: []api_v1.Container{ { @@ -1564,6 +1618,27 @@ func Test_extractPodContainersAttributes(t *testing.T) { pod: &pod, want: PodContainers{ByID: map[string]*Container{}, ByName: map[string]*Container{}}, }, + { + name: "operator-container-level-attributes", + rules: ExtractionRules{ + OperatorRules: OperatorRules{Enabled: true}, + }, + pod: &pod, + want: PodContainers{ + ByID: map[string]*Container{ + "container1-id-123": {ServiceName: "container1", ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, + "container2-id-456": {ServiceName: "container2", ServiceInstanceID: "test-namespace.test-pod.container2"}, + "container3-id-abc": {ServiceName: "container3", ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, + "init-container-id-789": {ServiceName: "init_container", ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, + }, + ByName: map[string]*Container{ + "container1": {ServiceName: "container1", ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, + "container2": {ServiceName: "container2", ServiceInstanceID: "test-namespace.test-pod.container2"}, + "container3": {ServiceName: "container3", ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, + "init_container": {ServiceName: "init_container", ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, + }, + }, + }, { name: "image-name-only", rules: ExtractionRules{ diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index 9faeee2452dd..4df7216648cd 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -117,7 +117,8 @@ type Pod struct { // Containers specifies all containers in this pod. Containers PodContainers - DeletedAt time.Time + DeletedAt time.Time + ServiceNames map[string]string } // PodContainers specifies a list of pod containers. It is not safe for concurrent use. @@ -130,9 +131,12 @@ type PodContainers struct { // Container stores resource attributes for a specific container defined by k8s pod spec. type Container struct { - Name string - ImageName string - ImageTag string + Name string + ImageName string + ImageTag string + ServiceInstanceID string + ServiceVersion string + ServiceName string // Statuses is a map of container k8s.container.restart_count attribute to ContainerStatus struct. Statuses map[int]ContainerStatus @@ -223,6 +227,8 @@ type ExtractionRules struct { Annotations []FieldExtractionRule Labels []FieldExtractionRule + + OperatorRules OperatorRules } // IncludesOwnerMetadata determines whether the ExtractionRules include metadata about Pod Owners @@ -245,7 +251,7 @@ func (rules *ExtractionRules) IncludesOwnerMetadata() bool { return true } } - return false + return rules.OperatorRules.Enabled } // FieldExtractionRule is used to specify which fields to extract from pod fields diff --git a/processor/k8sattributesprocessor/internal/kube/operator.go b/processor/k8sattributesprocessor/internal/kube/operator.go new file mode 100644 index 000000000000..6bcb03051081 --- /dev/null +++ b/processor/k8sattributesprocessor/internal/kube/operator.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kube // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor/internal/kube" + +import ( + "regexp" + "strings" + + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + v1 "k8s.io/api/core/v1" +) + +type OperatorRules struct { + Enabled bool `mapstructure:"enabled"` + Labels bool `mapstructure:"labels"` +} + +var OperatorAnnotationRule = FieldExtractionRule{ + Name: "$1", + KeyRegex: regexp.MustCompile(`^resource.opentelemetry.io/(.+)$`), + HasKeyRegexReference: true, + From: MetadataFromPod, +} + +var OperatorLabelRules = []FieldExtractionRule{ + { + Name: "service.name", + Key: "app.kubernetes.io/name", + From: MetadataFromPod, + }, + { + Name: "service.version", + Key: "app.kubernetes.io/version", + From: MetadataFromPod, + }, + { + Name: "service.namespace", + Key: "app.kubernetes.io/part-of", + From: MetadataFromPod, + }, +} + +var serviceNamePrecedence = []string{ + conventions.AttributeK8SDeploymentName, + conventions.AttributeK8SReplicaSetName, + conventions.AttributeK8SStatefulSetName, + conventions.AttributeK8SDaemonSetName, + conventions.AttributeK8SCronJobName, + conventions.AttributeK8SJobName, + conventions.AttributeK8SPodName, +} + +func OperatorServiceName(containerName string, names map[string]string) string { + for _, k := range serviceNamePrecedence { + if v, ok := names[k]; ok { + return v + } + } + return containerName +} + +func operatorServiceInstanceID(pod *v1.Pod, containerName string) string { + resNames := []string{pod.Namespace, pod.Name, containerName} + return strings.Join(resNames, ".") +} diff --git a/processor/k8sattributesprocessor/options.go b/processor/k8sattributesprocessor/options.go index 4ccccb0d4638..368aefd0098b 100644 --- a/processor/k8sattributesprocessor/options.go +++ b/processor/k8sattributesprocessor/options.go @@ -198,6 +198,19 @@ func withExtractMetadata(fields ...string) option { } } +func withOperatorExtractRules(rules kube.OperatorRules) option { + return func(p *kubernetesprocessor) error { + if rules.Enabled { + p.rules.OperatorRules = rules + p.rules.Annotations = append(p.rules.Annotations, kube.OperatorAnnotationRule) + if rules.Labels { + p.rules.Labels = append(p.rules.Labels, kube.OperatorLabelRules...) + } + } + return nil + } +} + // withExtractLabels allows specifying options to control extraction of pod labels. func withExtractLabels(labels ...FieldExtractConfig) option { return func(p *kubernetesprocessor) error { diff --git a/processor/k8sattributesprocessor/processor.go b/processor/k8sattributesprocessor/processor.go index e656a41469bc..1f776deb92a2 100644 --- a/processor/k8sattributesprocessor/processor.go +++ b/processor/k8sattributesprocessor/processor.go @@ -238,6 +238,15 @@ func (kp *kubernetesprocessor) addContainerAttributes(attrs pcommon.Map, pod *ku if containerSpec.ImageTag != "" { setResourceAttribute(attrs, conventions.AttributeContainerImageTag, containerSpec.ImageTag) } + if containerSpec.ServiceInstanceID != "" { + setResourceAttribute(attrs, conventions.AttributeServiceInstanceID, containerSpec.ServiceInstanceID) + } + if containerSpec.ServiceVersion != "" { + setResourceAttribute(attrs, conventions.AttributeServiceVersion, containerSpec.ServiceVersion) + } + if containerSpec.ServiceName != "" { + setResourceAttribute(attrs, conventions.AttributeServiceName, kube.OperatorServiceName(containerSpec.ServiceName, pod.ServiceNames)) + } // attempt to get container ID from restart count runID := -1 runIDAttr, ok := attrs.Get(conventions.AttributeK8SContainerRestartCount) diff --git a/processor/k8sattributesprocessor/processor_test.go b/processor/k8sattributesprocessor/processor_test.go index ee51cc82d9f0..52ab11148748 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -1034,9 +1034,12 @@ func TestProcessorAddContainerAttributes(t *testing.T) { Containers: kube.PodContainers{ ByName: map[string]*kube.Container{ "app": { - Name: "app", - ImageName: "test/app", - ImageTag: "1.0.1", + Name: "app", + ImageName: "test/app", + ImageTag: "1.0.1", + ServiceInstanceID: "instance-1", + ServiceVersion: "1.0.1", + ServiceName: "app", }, }, }, @@ -1051,6 +1054,9 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeK8SContainerName: "app", conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", + conventions.AttributeServiceInstanceID: "instance-1", + conventions.AttributeServiceVersion: "1.0.1", + conventions.AttributeServiceName: "app", }, }, { @@ -1091,6 +1097,56 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeContainerImageTag: "1.0.1", }, }, + { + name: "operator-explicit-values-win", + op: func(kp *kubernetesprocessor) { + kp.podAssociations = []kube.Association{ + { + Name: "k8s.pod.uid", + Sources: []kube.AssociationSource{ + { + From: "resource_attribute", + Name: "k8s.pod.uid", + }, + }, + }, + } + kp.kc.(*fakeClient).Pods[newPodIdentifier("resource_attribute", "k8s.pod.uid", "19f651bc-73e4-410f-b3e9-f0241679d3b8")] = &kube.Pod{ + Attributes: map[string]string{ + conventions.AttributeServiceInstanceID: "explicit-instance", + conventions.AttributeServiceVersion: "explicit-version", + conventions.AttributeServiceName: "explicit-name", + conventions.AttributeServiceNamespace: "explicit-ns", + }, + Containers: kube.PodContainers{ + ByID: map[string]*kube.Container{ + "767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd": { + Name: "app", + ImageName: "test/app", + ImageTag: "1.0.1", + ServiceInstanceID: "instance-1", + ServiceVersion: "version-1", + }, + }, + }, + } + }, + resourceGens: []generateResourceFunc{ + withPodUID("19f651bc-73e4-410f-b3e9-f0241679d3b8"), + withContainerID("767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd"), + }, + wantAttrs: map[string]any{ + conventions.AttributeK8SPodUID: "19f651bc-73e4-410f-b3e9-f0241679d3b8", + conventions.AttributeContainerID: "767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd", + conventions.AttributeK8SContainerName: "app", + conventions.AttributeContainerImageName: "test/app", + conventions.AttributeContainerImageTag: "1.0.1", + conventions.AttributeServiceInstanceID: "explicit-instance", + conventions.AttributeServiceVersion: "explicit-version", + conventions.AttributeServiceName: "explicit-name", + conventions.AttributeServiceNamespace: "explicit-ns", + }, + }, { name: "image-only", op: func(kp *kubernetesprocessor) { @@ -1267,31 +1323,37 @@ func TestProcessorAddContainerAttributes(t *testing.T) { } for _, tt := range tests { - m := newMultiTest( - t, - NewFactory().CreateDefaultConfig(), - nil, - ) - m.kubernetesProcessorOperation(tt.op) - m.testConsume(context.Background(), - generateTraces(tt.resourceGens...), - generateMetrics(tt.resourceGens...), - generateLogs(tt.resourceGens...), - generateProfiles(tt.resourceGens...), - nil, - ) - - m.assertBatchesLen(1) - m.assertResource(0, func(r pcommon.Resource) { - require.Equal(t, len(tt.wantAttrs), r.Attributes().Len()) - for k, v := range tt.wantAttrs { - switch val := v.(type) { - case string: - assertResourceHasStringAttribute(t, r, k, val) - case []string: - assertResourceHasStringSlice(t, r, k, val) + t.Run(tt.name, func(t *testing.T) { + m := newMultiTest( + t, + NewFactory().CreateDefaultConfig(), + nil, + withOperatorExtractRules(kube.OperatorRules{ + Enabled: true, + Labels: true, + }), + ) + m.kubernetesProcessorOperation(tt.op) + m.testConsume(context.Background(), + generateTraces(tt.resourceGens...), + generateMetrics(tt.resourceGens...), + generateLogs(tt.resourceGens...), + generateProfiles(tt.resourceGens...), + nil, + ) + + m.assertBatchesLen(1) + m.assertResource(0, func(r pcommon.Resource) { + require.Len(t, r.Attributes().AsRaw(), len(tt.wantAttrs)) + for k, v := range tt.wantAttrs { + switch val := v.(type) { + case string: + assertResourceHasStringAttribute(t, r, k, val) + case []string: + assertResourceHasStringSlice(t, r, k, val) + } } - } + }) }) } }