From a3c99232df7e1d920ebb332d1ea4d0827da7d2d1 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 8 Jan 2025 17:43:09 +0100 Subject: [PATCH 01/14] automatically set service instance id --- .../k8sattributesprocessor/internal/kube/client.go | 13 ++++++++++++- processor/k8sattributesprocessor/processor.go | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index a984e7f85cbd..a81ba048d25e 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "go.opentelemetry.io/otel/attribute" "regexp" "strings" "sync" @@ -571,6 +572,11 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string { return tags } +func createServiceInstanceID(pod *api_v1.Pod, containerName string) string { + resNames := []string{pod.Namespace, pod.Name, containerName} + return strings.Join(resNames, ".") +} + // This function removes all data from the Pod except what is required by extraction rules and pod association func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Pod { // name, namespace, uid, start time and ip are needed for identifying Pods @@ -722,6 +728,9 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain if c.Rules.ContainerName { container.Name = apiStatus.Name } + if c.Rules.AutoAnnotations { + container.ServiceInstanceID = createServiceInstanceID(pod, apiStatus.Name) + } containerID := apiStatus.ContainerID // Remove container runtime prefix parts := strings.Split(containerID, "://") @@ -1040,7 +1049,9 @@ func needContainerAttributes(rules ExtractionRules) bool { rules.ContainerName || rules.ContainerImageTag || rules.ContainerImageRepoDigests || - rules.ContainerID + rules.ContainerID || + rules.AutoAnnotations || + rules.AutoAll } func (c *WatchClient) handleReplicaSetAdd(obj any) { diff --git a/processor/k8sattributesprocessor/processor.go b/processor/k8sattributesprocessor/processor.go index e656a41469bc..f4fc7ac7494d 100644 --- a/processor/k8sattributesprocessor/processor.go +++ b/processor/k8sattributesprocessor/processor.go @@ -232,6 +232,9 @@ func (kp *kubernetesprocessor) addContainerAttributes(attrs pcommon.Map, pod *ku if containerSpec.Name != "" { setResourceAttribute(attrs, conventions.AttributeK8SContainerName, containerSpec.Name) } + if containerSpec.ServiceInstanceID != "" { + setResourceAttribute(attrs, conventions.AttributeServiceInstanceID, containerSpec.ServiceInstanceID) + } if containerSpec.ImageName != "" { setResourceAttribute(attrs, conventions.AttributeContainerImageName, containerSpec.ImageName) } From f0eea3db1323af796b078ab069a00ed0dbc700e4 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 8 Jan 2025 17:44:42 +0100 Subject: [PATCH 02/14] automatically set service instance id --- processor/k8sattributesprocessor/internal/kube/kube.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index 9faeee2452dd..ed8a294dff32 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -130,9 +130,10 @@ 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 // Statuses is a map of container k8s.container.restart_count attribute to ContainerStatus struct. Statuses map[int]ContainerStatus @@ -220,6 +221,8 @@ type ExtractionRules struct { ContainerImageRepoDigests bool ContainerImageTag bool ClusterUID bool + AutoAnnotations bool + AutoAll bool Annotations []FieldExtractionRule Labels []FieldExtractionRule From 89edad2f891495971fdac39061fc01f4f6bb7a79 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 07:30:23 +0100 Subject: [PATCH 03/14] automatically set service instance id --- processor/k8sattributesprocessor/config.go | 2 ++ processor/k8sattributesprocessor/factory.go | 1 + .../internal/kube/client.go | 6 ++--- .../internal/kube/client_test.go | 25 +++++++++++++++++++ .../internal/kube/kube.go | 9 +++++-- processor/k8sattributesprocessor/options.go | 9 +++++++ 6 files changed, 46 insertions(+), 6 deletions(-) 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 a81ba048d25e..d04debd6c5de 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "go.opentelemetry.io/otel/attribute" "regexp" "strings" "sync" @@ -728,7 +727,7 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain if c.Rules.ContainerName { container.Name = apiStatus.Name } - if c.Rules.AutoAnnotations { + if c.Rules.OperatorRules.Enabled { container.ServiceInstanceID = createServiceInstanceID(pod, apiStatus.Name) } containerID := apiStatus.ContainerID @@ -1050,8 +1049,7 @@ func needContainerAttributes(rules ExtractionRules) bool { rules.ContainerImageTag || rules.ContainerImageRepoDigests || rules.ContainerID || - rules.AutoAnnotations || - rules.AutoAll + 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..7fc6b3b63eef 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -1489,6 +1489,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 +1568,27 @@ func Test_extractPodContainersAttributes(t *testing.T) { pod: &pod, want: PodContainers{ByID: map[string]*Container{}, ByName: map[string]*Container{}}, }, + { + name: "service-instance-id-only", + rules: ExtractionRules{ + OperatorRules: OperatorRules{Enabled: true}, + }, + pod: &pod, + want: PodContainers{ + ByID: map[string]*Container{ + "container1-id-123": {ServiceInstanceID: "test-namespace.test-pod.container1"}, + "container2-id-456": {ServiceInstanceID: "test-namespace.test-pod.container2"}, + "container3-id-abc": {ServiceInstanceID: "test-namespace.test-pod.container3"}, + "init-container-id-789": {ServiceInstanceID: "test-namespace.test-pod.init_container"}, + }, + ByName: map[string]*Container{ + "container1": {ServiceInstanceID: "test-namespace.test-pod.container1"}, + "container2": {ServiceInstanceID: "test-namespace.test-pod.container2"}, + "container3": {ServiceInstanceID: "test-namespace.test-pod.container3"}, + "init_container": {ServiceInstanceID: "test-namespace.test-pod.init_container"}, + }, + }, + }, { name: "image-name-only", rules: ExtractionRules{ diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index ed8a294dff32..a4edefd4fd6c 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -193,6 +193,11 @@ type FieldFilter struct { Op selection.Operator } +type OperatorRules struct { + Enabled bool `mapstructure:"enabled"` + Labels bool `mapstructure:"labels"` +} + // ExtractionRules is used to specify the information that needs to be extracted // from pods and added to the spans as tags. type ExtractionRules struct { @@ -221,11 +226,11 @@ type ExtractionRules struct { ContainerImageRepoDigests bool ContainerImageTag bool ClusterUID bool - AutoAnnotations bool - AutoAll bool Annotations []FieldExtractionRule Labels []FieldExtractionRule + + OperatorRules OperatorRules } // IncludesOwnerMetadata determines whether the ExtractionRules include metadata about Pod Owners diff --git a/processor/k8sattributesprocessor/options.go b/processor/k8sattributesprocessor/options.go index 4ccccb0d4638..c53cd329c902 100644 --- a/processor/k8sattributesprocessor/options.go +++ b/processor/k8sattributesprocessor/options.go @@ -198,6 +198,15 @@ func withExtractMetadata(fields ...string) option { } } +func withOperatorExtractRules(rules kube.OperatorRules) option { + return func(p *kubernetesprocessor) error { + if rules.Enabled { + p.rules.OperatorRules = rules + } + return nil + } +} + // withExtractLabels allows specifying options to control extraction of pod labels. func withExtractLabels(labels ...FieldExtractConfig) option { return func(p *kubernetesprocessor) error { From 7ff75faf77a8fcb8d482243637bc5d76f97227a7 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 11:01:25 +0100 Subject: [PATCH 04/14] automatically set service instance id --- processor/k8sattributesprocessor/options.go | 6 + .../k8sattributesprocessor/processor_test.go | 113 +++++++++++++----- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/processor/k8sattributesprocessor/options.go b/processor/k8sattributesprocessor/options.go index c53cd329c902..c0c515a4afc0 100644 --- a/processor/k8sattributesprocessor/options.go +++ b/processor/k8sattributesprocessor/options.go @@ -202,6 +202,12 @@ 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.FieldExtractionRule{ + Name: "$1", + KeyRegex: regexp.MustCompile(`^resource.opentelemetry.io/(.+)$`), + HasKeyRegexReference: true, + From: kube.MetadataFromPod, + }) } return nil } diff --git a/processor/k8sattributesprocessor/processor_test.go b/processor/k8sattributesprocessor/processor_test.go index ee51cc82d9f0..a7ec406a5a41 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -1034,9 +1034,10 @@ 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", }, }, }, @@ -1051,6 +1052,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeK8SContainerName: "app", conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", + conventions.AttributeServiceInstanceID: "instance-1", }, }, { @@ -1071,9 +1073,10 @@ func TestProcessorAddContainerAttributes(t *testing.T) { Containers: kube.PodContainers{ ByID: map[string]*kube.Container{ "767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd": { - Name: "app", - ImageName: "test/app", - ImageTag: "1.0.1", + Name: "app", + ImageName: "test/app", + ImageTag: "1.0.1", + ServiceInstanceID: "instance-1", }, }, }, @@ -1089,6 +1092,50 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeK8SContainerName: "app", conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", + conventions.AttributeServiceInstanceID: "instance-1", + }, + }, + { + name: "explicit-service-instance-id", + 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{ + "service.instance.id": "explicit-1", + }, + Containers: kube.PodContainers{ + ByID: map[string]*kube.Container{ + "767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd": { + Name: "app", + ImageName: "test/app", + ImageTag: "1.0.1", + ServiceInstanceID: "instance-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-1", }, }, { @@ -1267,31 +1314,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) + } } - } + }) }) } } From 1b690ac1b7b276a88d2cedeaf0ca98972717b3ca Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 12:05:07 +0100 Subject: [PATCH 05/14] add label rules, test annotation override --- .../internal/kube/client_test.go | 60 +++++++++++++++---- .../internal/kube/kube.go | 25 ++++++++ processor/k8sattributesprocessor/options.go | 10 ++-- 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 7fc6b3b63eef..7c84dd63bea7 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -643,8 +643,11 @@ func TestExtractionRules(t *testing.T) { Namespace: "ns1", CreationTimestamp: meta_v1.Now(), Labels: map[string]string{ - "label1": "lv1", - "label2": "k1=v1 k5=v5 extra!", + "label1": "lv1", + "label2": "k1=v1 k5=v5 extra!", + "app.kubernetes.io/name": "auth-service", + "app.kubernetes.io/version": "1.0.0", + "app.kubernetes.io/part-of": "auth", }, Annotations: map[string]string{ "annotation1": "av1", @@ -703,9 +706,10 @@ func TestExtractionRules(t *testing.T) { } testCases := []struct { - name string - rules ExtractionRules - attributes map[string]string + name string + rules ExtractionRules + additionalAnnotations map[string]string + attributes map[string]string }{ { name: "no-rules", @@ -962,6 +966,41 @@ func TestExtractionRules(t *testing.T) { "prefix-annotation1": "av1", }, }, + { + name: "operator-rules", + rules: ExtractionRules{ + Annotations: []FieldExtractionRule{OperatorAnnotationRule}, + Labels: OperatorLabelRules, + }, + additionalAnnotations: map[string]string{ + "resource.opentelemetry.io/service.instance.id": "instance-id", + }, + attributes: map[string]string{ + "service.instance.id": "instance-id", + "service.name": "auth-service", + "service.version": "1.0.0", + "service.namespace": "auth", + }, + }, + { + name: "operator-rules-annotation-override", + rules: ExtractionRules{ + Annotations: []FieldExtractionRule{OperatorAnnotationRule}, + Labels: OperatorLabelRules, + }, + additionalAnnotations: map[string]string{ + "resource.opentelemetry.io/service.instance.id": "instance-id", + "resource.opentelemetry.io/service.version": "1.1.0", + "resource.opentelemetry.io/service.name": "auth-service2", + "resource.opentelemetry.io/service.namespace": "auth2", + }, + attributes: map[string]string{ + "service.instance.id": "instance-id", + "service.name": "auth-service2", + "service.version": "1.0.0", + "service.namespace": "auth2", + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -969,6 +1008,10 @@ 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 + pod := pod.DeepCopy() + for k, v := range tc.additionalAnnotations { + pod.Annotations[k] = v + } transformedPod := removeUnnecessaryPodData(pod, c.Rules) transformedReplicaset := removeUnnecessaryReplicaSetData(replicaset) c.handleReplicaSetAdd(transformedReplicaset) @@ -976,12 +1019,7 @@ func TestExtractionRules(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) }) } } diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index a4edefd4fd6c..5fa44d3f40e0 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -277,6 +277,31 @@ type FieldExtractionRule struct { From string } +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, + }, +} + func (r *FieldExtractionRule) extractFromPodMetadata(metadata map[string]string, tags map[string]string, formatter string) { // By default if the From field is not set for labels and annotations we want to extract them from pod if r.From == MetadataFromPod || r.From == "" { diff --git a/processor/k8sattributesprocessor/options.go b/processor/k8sattributesprocessor/options.go index c0c515a4afc0..368aefd0098b 100644 --- a/processor/k8sattributesprocessor/options.go +++ b/processor/k8sattributesprocessor/options.go @@ -202,12 +202,10 @@ 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.FieldExtractionRule{ - Name: "$1", - KeyRegex: regexp.MustCompile(`^resource.opentelemetry.io/(.+)$`), - HasKeyRegexReference: true, - From: kube.MetadataFromPod, - }) + p.rules.Annotations = append(p.rules.Annotations, kube.OperatorAnnotationRule) + if rules.Labels { + p.rules.Labels = append(p.rules.Labels, kube.OperatorLabelRules...) + } } return nil } From 0f6042225ed197e3845d35fc36ca0ce279078da0 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 12:33:51 +0100 Subject: [PATCH 06/14] add service version --- .../k8sattributesprocessor/internal/kube/client.go | 7 +++++-- .../internal/kube/client_test.go | 14 +++++++------- .../k8sattributesprocessor/internal/kube/kube.go | 1 + processor/k8sattributesprocessor/processor_test.go | 8 ++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index d04debd6c5de..730612585941 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -638,7 +638,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 @@ -703,7 +703,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) @@ -714,6 +714,9 @@ 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 } diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 7c84dd63bea7..41849268f291 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -1607,23 +1607,23 @@ func Test_extractPodContainersAttributes(t *testing.T) { want: PodContainers{ByID: map[string]*Container{}, ByName: map[string]*Container{}}, }, { - name: "service-instance-id-only", + name: "operator-container-level-attributes", rules: ExtractionRules{ OperatorRules: OperatorRules{Enabled: true}, }, pod: &pod, want: PodContainers{ ByID: map[string]*Container{ - "container1-id-123": {ServiceInstanceID: "test-namespace.test-pod.container1"}, + "container1-id-123": {ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, "container2-id-456": {ServiceInstanceID: "test-namespace.test-pod.container2"}, - "container3-id-abc": {ServiceInstanceID: "test-namespace.test-pod.container3"}, - "init-container-id-789": {ServiceInstanceID: "test-namespace.test-pod.init_container"}, + "container3-id-abc": {ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, + "init-container-id-789": {ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, }, ByName: map[string]*Container{ - "container1": {ServiceInstanceID: "test-namespace.test-pod.container1"}, + "container1": {ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, "container2": {ServiceInstanceID: "test-namespace.test-pod.container2"}, - "container3": {ServiceInstanceID: "test-namespace.test-pod.container3"}, - "init_container": {ServiceInstanceID: "test-namespace.test-pod.init_container"}, + "container3": {ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, + "init_container": {ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, }, }, }, diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index 5fa44d3f40e0..2540b1878a9d 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -134,6 +134,7 @@ type Container struct { ImageName string ImageTag string ServiceInstanceID string + ServiceVersion string // Statuses is a map of container k8s.container.restart_count attribute to ContainerStatus struct. Statuses map[int]ContainerStatus diff --git a/processor/k8sattributesprocessor/processor_test.go b/processor/k8sattributesprocessor/processor_test.go index a7ec406a5a41..410efc9aff49 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -1038,6 +1038,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { ImageName: "test/app", ImageTag: "1.0.1", ServiceInstanceID: "instance-1", + ServiceVersion: "1.0.1", }, }, }, @@ -1053,6 +1054,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", conventions.AttributeServiceInstanceID: "instance-1", + conventions.AttributeServiceVersion: "1.0.1", }, }, { @@ -1093,6 +1095,8 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", conventions.AttributeServiceInstanceID: "instance-1", + conventions.AttributeServiceVersion: "1.0.1", + conventions.AttributeServiceName: "app", }, }, { @@ -1112,6 +1116,8 @@ func TestProcessorAddContainerAttributes(t *testing.T) { kp.kc.(*fakeClient).Pods[newPodIdentifier("resource_attribute", "k8s.pod.uid", "19f651bc-73e4-410f-b3e9-f0241679d3b8")] = &kube.Pod{ Attributes: map[string]string{ "service.instance.id": "explicit-1", + "service.version": "1.0.2", + "service.name": "test-app", }, Containers: kube.PodContainers{ ByID: map[string]*kube.Container{ @@ -1136,6 +1142,8 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", conventions.AttributeServiceInstanceID: "explicit-1", + conventions.AttributeServiceVersion: "1.0.2", + conventions.AttributeServiceName: "test-app", }, }, { From 1d676dd13d9017b025fbaa3028db306a9cdc6b89 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 15:03:51 +0100 Subject: [PATCH 07/14] automatically set service name --- .../internal/kube/client.go | 63 ++++++++---- .../internal/kube/client_test.go | 99 ++++++++++--------- .../internal/kube/kube.go | 6 +- .../internal/kube/operator.go | 31 ++++++ processor/k8sattributesprocessor/processor.go | 12 ++- .../k8sattributesprocessor/processor_test.go | 29 +++--- 6 files changed, 159 insertions(+), 81 deletions(-) create mode 100644 processor/k8sattributesprocessor/internal/kube/operator.go diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index 730612585941..676fbc199c59 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,12 +508,14 @@ 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 - } - } + tags[conventions.AttributeK8SDeploymentName] = c.deploymentName(ref) + } + if c.Rules.OperatorRules.Enabled { + serviceNames[conventions.AttributeK8SDeploymentName] = c.deploymentName(ref) } if c.Rules.DeploymentUID { if replicaset, ok := c.getReplicaSet(string(ref.UID)); ok { @@ -525,6 +531,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 +541,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 +563,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,12 +589,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 createServiceInstanceID(pod *api_v1.Pod, containerName string) string { - resNames := []string{pod.Namespace, pod.Name, containerName} - return strings.Join(resNames, ".") +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 @@ -722,16 +747,18 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain } } 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 = createServiceInstanceID(pod, apiStatus.Name) + container.ServiceInstanceID = createServiceInstanceID(pod, containerName) + container.ServiceName = containerName } containerID := apiStatus.ContainerID // Remove container runtime prefix @@ -807,7 +834,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) } diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 41849268f291..8625a9cdd149 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -643,11 +643,8 @@ func TestExtractionRules(t *testing.T) { Namespace: "ns1", CreationTimestamp: meta_v1.Now(), Labels: map[string]string{ - "label1": "lv1", - "label2": "k1=v1 k5=v5 extra!", - "app.kubernetes.io/name": "auth-service", - "app.kubernetes.io/version": "1.0.0", - "app.kubernetes.io/part-of": "auth", + "label1": "lv1", + "label2": "k1=v1 k5=v5 extra!", }, Annotations: map[string]string{ "annotation1": "av1", @@ -705,16 +702,26 @@ func TestExtractionRules(t *testing.T) { }, } + operatorRules := ExtractionRules{ + OperatorRules: OperatorRules{ + Enabled: true, + Labels: true}, + Annotations: []FieldExtractionRule{OperatorAnnotationRule}, + Labels: OperatorLabelRules, + } + testCases := []struct { 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", @@ -967,38 +974,41 @@ func TestExtractionRules(t *testing.T) { }, }, { - name: "operator-rules", - rules: ExtractionRules{ - Annotations: []FieldExtractionRule{OperatorAnnotationRule}, - Labels: OperatorLabelRules, + name: "operator-rules-builtin", + rules: operatorRules, + attributes: map[string]string{ + // tested in operator-container-level-attributes below }, - additionalAnnotations: map[string]string{ - "resource.opentelemetry.io/service.instance.id": "instance-id", + 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.instance.id": "instance-id", - "service.name": "auth-service", - "service.version": "1.0.0", - "service.namespace": "auth", + "service.name": "label-service", + "service.version": "label-version", + "service.namespace": "label-namespace", }, }, { - name: "operator-rules-annotation-override", - rules: ExtractionRules{ - Annotations: []FieldExtractionRule{OperatorAnnotationRule}, - Labels: OperatorLabelRules, - }, + name: "operator-rules-annotation-override", + rules: operatorRules, additionalAnnotations: map[string]string{ - "resource.opentelemetry.io/service.instance.id": "instance-id", - "resource.opentelemetry.io/service.version": "1.1.0", - "resource.opentelemetry.io/service.name": "auth-service2", - "resource.opentelemetry.io/service.namespace": "auth2", + "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": "instance-id", - "service.name": "auth-service2", - "service.version": "1.0.0", - "service.namespace": "auth2", + "service.instance.id": "annotation-id", + "service.name": "annotation-service", + "service.version": "annotation-version", + "service.namespace": "annotation-namespace", }, }, } @@ -1012,6 +1022,9 @@ func TestExtractionRules(t *testing.T) { for k, v := range tc.additionalAnnotations { pod.Annotations[k] = v } + for k, v := range tc.additionalLabels { + pod.Labels[k] = v + } transformedPod := removeUnnecessaryPodData(pod, c.Rules) transformedReplicaset := removeUnnecessaryReplicaSetData(replicaset) c.handleReplicaSetAdd(transformedReplicaset) @@ -1020,6 +1033,9 @@ func TestExtractionRules(t *testing.T) { require.True(t, ok) assert.Equal(t, tc.attributes, p.Attributes) + if tc.serviceName != "" { + assert.Equal(t, tc.serviceName, OperatorServiceName("containerName", p.ServiceNames)) + } }) } } @@ -1078,7 +1094,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{ @@ -1170,12 +1186,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) }) } } @@ -1614,16 +1625,16 @@ func Test_extractPodContainersAttributes(t *testing.T) { pod: &pod, want: PodContainers{ ByID: map[string]*Container{ - "container1-id-123": {ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, - "container2-id-456": {ServiceInstanceID: "test-namespace.test-pod.container2"}, - "container3-id-abc": {ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, - "init-container-id-789": {ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, + "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": {ServiceInstanceID: "test-namespace.test-pod.container1", ServiceVersion: "0.1.0"}, - "container2": {ServiceInstanceID: "test-namespace.test-pod.container2"}, - "container3": {ServiceInstanceID: "test-namespace.test-pod.container3", ServiceVersion: "1.0"}, - "init_container": {ServiceInstanceID: "test-namespace.test-pod.init_container", ServiceVersion: "latest"}, + "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"}, }, }, }, diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index 2540b1878a9d..aa0114930cba 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. @@ -135,6 +136,7 @@ type Container struct { 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 @@ -254,7 +256,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..ef3660f5dfd5 --- /dev/null +++ b/processor/k8sattributesprocessor/internal/kube/operator.go @@ -0,0 +1,31 @@ +package kube + +import ( + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + "k8s.io/api/core/v1" + "strings" +) + +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 createServiceInstanceID(pod *v1.Pod, containerName string) string { + resNames := []string{pod.Namespace, pod.Name, containerName} + return strings.Join(resNames, ".") +} diff --git a/processor/k8sattributesprocessor/processor.go b/processor/k8sattributesprocessor/processor.go index f4fc7ac7494d..1f776deb92a2 100644 --- a/processor/k8sattributesprocessor/processor.go +++ b/processor/k8sattributesprocessor/processor.go @@ -232,15 +232,21 @@ func (kp *kubernetesprocessor) addContainerAttributes(attrs pcommon.Map, pod *ku if containerSpec.Name != "" { setResourceAttribute(attrs, conventions.AttributeK8SContainerName, containerSpec.Name) } - if containerSpec.ServiceInstanceID != "" { - setResourceAttribute(attrs, conventions.AttributeServiceInstanceID, containerSpec.ServiceInstanceID) - } if containerSpec.ImageName != "" { setResourceAttribute(attrs, conventions.AttributeContainerImageName, containerSpec.ImageName) } 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 410efc9aff49..52ab11148748 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -1039,6 +1039,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { ImageTag: "1.0.1", ServiceInstanceID: "instance-1", ServiceVersion: "1.0.1", + ServiceName: "app", }, }, }, @@ -1055,6 +1056,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeContainerImageTag: "1.0.1", conventions.AttributeServiceInstanceID: "instance-1", conventions.AttributeServiceVersion: "1.0.1", + conventions.AttributeServiceName: "app", }, }, { @@ -1075,10 +1077,9 @@ func TestProcessorAddContainerAttributes(t *testing.T) { Containers: kube.PodContainers{ ByID: map[string]*kube.Container{ "767dc30d4fece77038e8ec2585a33471944d0b754659af7aa7e101181418f0dd": { - Name: "app", - ImageName: "test/app", - ImageTag: "1.0.1", - ServiceInstanceID: "instance-1", + Name: "app", + ImageName: "test/app", + ImageTag: "1.0.1", }, }, }, @@ -1094,13 +1095,10 @@ 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", }, }, { - name: "explicit-service-instance-id", + name: "operator-explicit-values-win", op: func(kp *kubernetesprocessor) { kp.podAssociations = []kube.Association{ { @@ -1115,9 +1113,10 @@ func TestProcessorAddContainerAttributes(t *testing.T) { } kp.kc.(*fakeClient).Pods[newPodIdentifier("resource_attribute", "k8s.pod.uid", "19f651bc-73e4-410f-b3e9-f0241679d3b8")] = &kube.Pod{ Attributes: map[string]string{ - "service.instance.id": "explicit-1", - "service.version": "1.0.2", - "service.name": "test-app", + conventions.AttributeServiceInstanceID: "explicit-instance", + conventions.AttributeServiceVersion: "explicit-version", + conventions.AttributeServiceName: "explicit-name", + conventions.AttributeServiceNamespace: "explicit-ns", }, Containers: kube.PodContainers{ ByID: map[string]*kube.Container{ @@ -1126,6 +1125,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { ImageName: "test/app", ImageTag: "1.0.1", ServiceInstanceID: "instance-1", + ServiceVersion: "version-1", }, }, }, @@ -1141,9 +1141,10 @@ func TestProcessorAddContainerAttributes(t *testing.T) { conventions.AttributeK8SContainerName: "app", conventions.AttributeContainerImageName: "test/app", conventions.AttributeContainerImageTag: "1.0.1", - conventions.AttributeServiceInstanceID: "explicit-1", - conventions.AttributeServiceVersion: "1.0.2", - conventions.AttributeServiceName: "test-app", + conventions.AttributeServiceInstanceID: "explicit-instance", + conventions.AttributeServiceVersion: "explicit-version", + conventions.AttributeServiceName: "explicit-name", + conventions.AttributeServiceNamespace: "explicit-ns", }, }, { From c22d45c9ce3829b1ee1f6f59dc7dbbd0987df31d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 15:08:32 +0100 Subject: [PATCH 08/14] automatically set service name --- .../internal/kube/client.go | 2 +- .../internal/kube/client_test.go | 3 +- .../internal/kube/kube.go | 30 ----------------- .../internal/kube/operator.go | 33 ++++++++++++++++++- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index 676fbc199c59..8effe2037e1d 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -757,7 +757,7 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain container.Name = containerName } if c.Rules.OperatorRules.Enabled { - container.ServiceInstanceID = createServiceInstanceID(pod, containerName) + container.ServiceInstanceID = operatorServiceInstanceID(pod, containerName) container.ServiceName = containerName } containerID := apiStatus.ContainerID diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 8625a9cdd149..49e316a9d2e5 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -705,7 +705,8 @@ func TestExtractionRules(t *testing.T) { operatorRules := ExtractionRules{ OperatorRules: OperatorRules{ Enabled: true, - Labels: true}, + Labels: true, + }, Annotations: []FieldExtractionRule{OperatorAnnotationRule}, Labels: OperatorLabelRules, } diff --git a/processor/k8sattributesprocessor/internal/kube/kube.go b/processor/k8sattributesprocessor/internal/kube/kube.go index aa0114930cba..4df7216648cd 100644 --- a/processor/k8sattributesprocessor/internal/kube/kube.go +++ b/processor/k8sattributesprocessor/internal/kube/kube.go @@ -196,11 +196,6 @@ type FieldFilter struct { Op selection.Operator } -type OperatorRules struct { - Enabled bool `mapstructure:"enabled"` - Labels bool `mapstructure:"labels"` -} - // ExtractionRules is used to specify the information that needs to be extracted // from pods and added to the spans as tags. type ExtractionRules struct { @@ -280,31 +275,6 @@ type FieldExtractionRule struct { From string } -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, - }, -} - func (r *FieldExtractionRule) extractFromPodMetadata(metadata map[string]string, tags map[string]string, formatter string) { // By default if the From field is not set for labels and annotations we want to extract them from pod if r.From == MetadataFromPod || r.From == "" { diff --git a/processor/k8sattributesprocessor/internal/kube/operator.go b/processor/k8sattributesprocessor/internal/kube/operator.go index ef3660f5dfd5..0bccb2c3b8d2 100644 --- a/processor/k8sattributesprocessor/internal/kube/operator.go +++ b/processor/k8sattributesprocessor/internal/kube/operator.go @@ -3,9 +3,40 @@ package kube import ( conventions "go.opentelemetry.io/collector/semconv/v1.6.1" "k8s.io/api/core/v1" + "regexp" "strings" ) +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, @@ -25,7 +56,7 @@ func OperatorServiceName(containerName string, names map[string]string) string { return containerName } -func createServiceInstanceID(pod *v1.Pod, containerName string) string { +func operatorServiceInstanceID(pod *v1.Pod, containerName string) string { resNames := []string{pod.Namespace, pod.Name, containerName} return strings.Join(resNames, ".") } From 18b99c0250c31eae947d8cffcc6dac6056e5056d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 15:11:47 +0100 Subject: [PATCH 09/14] automatically set service name --- processor/k8sattributesprocessor/internal/kube/operator.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/operator.go b/processor/k8sattributesprocessor/internal/kube/operator.go index 0bccb2c3b8d2..dc0935bd3cf4 100644 --- a/processor/k8sattributesprocessor/internal/kube/operator.go +++ b/processor/k8sattributesprocessor/internal/kube/operator.go @@ -1,10 +1,11 @@ package kube import ( - conventions "go.opentelemetry.io/collector/semconv/v1.6.1" - "k8s.io/api/core/v1" "regexp" "strings" + + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + v1 "k8s.io/api/core/v1" ) type OperatorRules struct { From 98b29ee8ece676c55a8aa1e6de598ba6d12eef36 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 15:14:52 +0100 Subject: [PATCH 10/14] automatically set service name --- processor/k8sattributesprocessor/internal/kube/operator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/processor/k8sattributesprocessor/internal/kube/operator.go b/processor/k8sattributesprocessor/internal/kube/operator.go index dc0935bd3cf4..6bcb03051081 100644 --- a/processor/k8sattributesprocessor/internal/kube/operator.go +++ b/processor/k8sattributesprocessor/internal/kube/operator.go @@ -1,4 +1,7 @@ -package kube +// 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" From 926ff32b8da2d104a2cf15b1fce93126cb950980 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 9 Jan 2025 16:14:04 +0100 Subject: [PATCH 11/14] automatically set service name --- .../k8sattributesprocessor/internal/kube/client.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index 8effe2037e1d..df84c94d3970 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -512,10 +512,16 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) (map[string]string, serviceNames[conventions.AttributeK8SReplicaSetName] = ref.Name } if c.Rules.DeploymentName { - tags[conventions.AttributeK8SDeploymentName] = c.deploymentName(ref) + name := c.deploymentName(ref) + if name != "" { + tags[conventions.AttributeK8SDeploymentName] = name + } } if c.Rules.OperatorRules.Enabled { - serviceNames[conventions.AttributeK8SDeploymentName] = c.deploymentName(ref) + name := c.deploymentName(ref) + if name != "" { + serviceNames[conventions.AttributeK8SDeploymentName] = name + } } if c.Rules.DeploymentUID { if replicaset, ok := c.getReplicaSet(string(ref.UID)); ok { From b2b46dc514917dc023fcdd622db5bf4dabbc088d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 10 Jan 2025 09:30:08 +0100 Subject: [PATCH 12/14] lint --- .../internal/kube/client_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 49e316a9d2e5..fb7faf9af092 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -1019,18 +1019,18 @@ 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 - pod := pod.DeepCopy() + podCopy := pod.DeepCopy() for k, v := range tc.additionalAnnotations { - pod.Annotations[k] = v + podCopy.Annotations[k] = v } for k, v := range tc.additionalLabels { - pod.Labels[k] = v + podCopy.Labels[k] = v } - transformedPod := removeUnnecessaryPodData(pod, c.Rules) + 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, tc.attributes, p.Attributes) From d4dc77d2958e8293158def1670bcee2fafe70043 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 10 Jan 2025 13:37:01 +0100 Subject: [PATCH 13/14] add changelog entry --- .chloggen/operator-resource-attributes.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .chloggen/operator-resource-attributes.yaml diff --git a/.chloggen/operator-resource-attributes.yaml b/.chloggen/operator-resource-attributes.yaml new file mode 100644 index 000000000000..38681c76469a --- /dev/null +++ b/.chloggen/operator-resource-attributes.yaml @@ -0,0 +1,12 @@ +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. From 61b534d13c0366fc3c533b2d8de287a62b312baa Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 10 Jan 2025 14:08:56 +0100 Subject: [PATCH 14/14] add docs --- .chloggen/operator-resource-attributes.yaml | 1 + processor/k8sattributesprocessor/README.md | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.chloggen/operator-resource-attributes.yaml b/.chloggen/operator-resource-attributes.yaml index 38681c76469a..789d52cc5cbf 100644 --- a/.chloggen/operator-resource-attributes.yaml +++ b/.chloggen/operator-resource-attributes.yaml @@ -10,3 +10,4 @@ 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.