Skip to content

Commit 99fb6b6

Browse files
[processor/k8sattributes] Add optional k8s.cluster.uid resource attribute (#23668)
**Description:** Add k8s.cluster.uid to attribute to k8sattributes processor and disable it by default for backward compatibility. Users can set it to `true` to populate cluster uid as part of resource attributes. **Link to tracking Issue:** #21974
1 parent a6cc5ee commit 99fb6b6

File tree

18 files changed

+129
-8
lines changed

18 files changed

+129
-8
lines changed

.chloggen/k8smetadata.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: k8sattributes
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Added k8s.cluster.uid to k8sattributes processor to add cluster uid
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [21974]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

examples/kubernetes/otel-collector.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ data:
9595
- k8s.namespace.name
9696
- k8s.node.name
9797
- k8s.pod.start_time
98+
- k8s.cluster.uid
9899
# Pod labels which can be fetched via K8sattributeprocessor
99100
labels:
100101
- tag_name: key1

processor/k8sattributesprocessor/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type ExtractConfig struct {
6666
// k8s.statefulset.name, k8s.statefulset.uid,
6767
// k8s.container.name, container.image.name,
6868
// container.image.tag, container.id
69+
// k8s.cluster.uid
6970
//
7071
// Specifying anything other than these values will result in an error.
7172
// By default, the following fields are extracted and added to spans, metrics and logs as attributes:

processor/k8sattributesprocessor/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func TestLoadConfig(t *testing.T) {
3737
APIConfig: k8sconfig.APIConfig{AuthType: k8sconfig.AuthTypeKubeConfig},
3838
Passthrough: false,
3939
Extract: ExtractConfig{
40-
Metadata: []string{"k8s.pod.name", "k8s.pod.uid", "k8s.deployment.name", "k8s.namespace.name", "k8s.node.name", "k8s.pod.start_time"},
40+
Metadata: []string{"k8s.pod.name", "k8s.pod.uid", "k8s.deployment.name", "k8s.namespace.name", "k8s.node.name", "k8s.pod.start_time", "k8s.cluster.uid"},
4141
Annotations: []FieldExtractConfig{
4242
{TagName: "a1", Key: "annotation-one", From: "pod"},
4343
{TagName: "a2", Key: "annotation-two", Regex: "field=(?P<value>.+)", From: kube.MetadataFromPod},

processor/k8sattributesprocessor/e2e_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func TestE2E(t *testing.T) {
103103
"k8s.annotations.workload": newExpectedValue(equal, "job"),
104104
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-traces-job"),
105105
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
106+
"k8s.cluster.uid": newExpectedValue(exist, ""),
106107
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
107108
"container.image.tag": newExpectedValue(equal, "latest"),
108109
"container.id": newExpectedValue(exist, ""),
@@ -124,6 +125,7 @@ func TestE2E(t *testing.T) {
124125
"k8s.annotations.workload": newExpectedValue(equal, "statefulset"),
125126
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-traces-statefulset"),
126127
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
128+
"k8s.cluster.uid": newExpectedValue(exist, ""),
127129
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
128130
"container.image.tag": newExpectedValue(equal, "latest"),
129131
"container.id": newExpectedValue(exist, ""),
@@ -147,6 +149,7 @@ func TestE2E(t *testing.T) {
147149
"k8s.annotations.workload": newExpectedValue(equal, "deployment"),
148150
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-traces-deployment"),
149151
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
152+
"k8s.cluster.uid": newExpectedValue(exist, ""),
150153
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
151154
"container.image.tag": newExpectedValue(equal, "latest"),
152155
"container.id": newExpectedValue(exist, ""),
@@ -168,6 +171,7 @@ func TestE2E(t *testing.T) {
168171
"k8s.annotations.workload": newExpectedValue(equal, "daemonset"),
169172
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-traces-daemonset"),
170173
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
174+
"k8s.cluster.uid": newExpectedValue(exist, ""),
171175
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
172176
"container.image.tag": newExpectedValue(equal, "latest"),
173177
"container.id": newExpectedValue(exist, ""),
@@ -189,6 +193,7 @@ func TestE2E(t *testing.T) {
189193
"k8s.annotations.workload": newExpectedValue(equal, "job"),
190194
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-metrics-job"),
191195
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
196+
"k8s.cluster.uid": newExpectedValue(exist, ""),
192197
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
193198
"container.image.tag": newExpectedValue(equal, "latest"),
194199
"container.id": newExpectedValue(exist, ""),
@@ -210,6 +215,7 @@ func TestE2E(t *testing.T) {
210215
"k8s.annotations.workload": newExpectedValue(equal, "statefulset"),
211216
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-metrics-statefulset"),
212217
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
218+
"k8s.cluster.uid": newExpectedValue(exist, ""),
213219
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
214220
"container.image.tag": newExpectedValue(equal, "latest"),
215221
"container.id": newExpectedValue(exist, ""),
@@ -233,6 +239,7 @@ func TestE2E(t *testing.T) {
233239
"k8s.annotations.workload": newExpectedValue(equal, "deployment"),
234240
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-metrics-deployment"),
235241
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
242+
"k8s.cluster.uid": newExpectedValue(exist, ""),
236243
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
237244
"container.image.tag": newExpectedValue(equal, "latest"),
238245
"container.id": newExpectedValue(exist, ""),
@@ -254,6 +261,7 @@ func TestE2E(t *testing.T) {
254261
"k8s.annotations.workload": newExpectedValue(equal, "daemonset"),
255262
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-metrics-daemonset"),
256263
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
264+
"k8s.cluster.uid": newExpectedValue(exist, ""),
257265
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
258266
"container.image.tag": newExpectedValue(equal, "latest"),
259267
"container.id": newExpectedValue(exist, ""),
@@ -275,6 +283,7 @@ func TestE2E(t *testing.T) {
275283
"k8s.annotations.workload": newExpectedValue(equal, "job"),
276284
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-logs-job"),
277285
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
286+
"k8s.cluster.uid": newExpectedValue(exist, ""),
278287
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
279288
"container.image.tag": newExpectedValue(equal, "latest"),
280289
"container.id": newExpectedValue(exist, ""),
@@ -296,6 +305,7 @@ func TestE2E(t *testing.T) {
296305
"k8s.annotations.workload": newExpectedValue(equal, "statefulset"),
297306
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-logs-statefulset"),
298307
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
308+
"k8s.cluster.uid": newExpectedValue(exist, ""),
299309
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
300310
"container.image.tag": newExpectedValue(equal, "latest"),
301311
"container.id": newExpectedValue(exist, ""),
@@ -319,6 +329,7 @@ func TestE2E(t *testing.T) {
319329
"k8s.annotations.workload": newExpectedValue(equal, "deployment"),
320330
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-logs-deployment"),
321331
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
332+
"k8s.cluster.uid": newExpectedValue(exist, ""),
322333
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
323334
"container.image.tag": newExpectedValue(equal, "latest"),
324335
"container.id": newExpectedValue(exist, ""),
@@ -340,6 +351,7 @@ func TestE2E(t *testing.T) {
340351
"k8s.annotations.workload": newExpectedValue(equal, "daemonset"),
341352
"k8s.labels.app": newExpectedValue(equal, "telemetrygen-"+testID+"-logs-daemonset"),
342353
"k8s.container.name": newExpectedValue(equal, "telemetrygen"),
354+
"k8s.cluster.uid": newExpectedValue(exist, ""),
343355
"container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"),
344356
"container.image.tag": newExpectedValue(equal, "latest"),
345357
"container.id": newExpectedValue(exist, ""),

processor/k8sattributesprocessor/internal/kube/client.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ func New(logger *zap.Logger, apiCfg k8sconfig.APIConfig, rules ExtractionRules,
114114
}
115115

116116
if newNamespaceInformer == nil {
117-
newNamespaceInformer = newNamespaceSharedInformer
117+
// if rules to extract metadata from namespace is configured use namespace shared informer containing
118+
// all namespaces including kube-system which contains cluster uid information (kube-system-uid)
119+
if c.extractNamespaceLabelsAnnotations() {
120+
newNamespaceInformer = newNamespaceSharedInformer
121+
} else {
122+
// use kube-system shared informer to only watch kube-system namespace
123+
// reducing overhead of watching all the namespaces
124+
newNamespaceInformer = newKubeSystemSharedInformer
125+
}
118126
}
119127

120128
c.informer = newInformer(c.kc, c.Filters.Namespace, labelSelector, fieldSelector)
@@ -132,11 +140,7 @@ func New(logger *zap.Logger, apiCfg k8sconfig.APIConfig, rules ExtractionRules,
132140
return nil, err
133141
}
134142

135-
if c.extractNamespaceLabelsAnnotations() {
136-
c.namespaceInformer = newNamespaceInformer(c.kc)
137-
} else {
138-
c.namespaceInformer = NewNoOpInformer(c.kc)
139-
}
143+
c.namespaceInformer = newNamespaceInformer(c.kc)
140144

141145
if rules.DeploymentName || rules.DeploymentUID {
142146
if newReplicaSetInformer == nil {
@@ -433,6 +437,14 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
433437
tags[tagNodeName] = pod.Spec.NodeName
434438
}
435439

440+
if c.Rules.ClusterUID {
441+
if val, ok := c.Namespaces["kube-system"]; ok {
442+
tags[tagClusterUID] = val.NamespaceUID
443+
} else {
444+
c.logger.Debug("unable to find kube-system namespace, cluster uid will not be available")
445+
}
446+
}
447+
436448
for _, r := range c.Rules.Labels {
437449
r.extractFromPodMetadata(pod.Labels, tags, "k8s.pod.labels.%s")
438450
}

processor/k8sattributesprocessor/internal/kube/informer.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"k8s.io/client-go/tools/cache"
1818
)
1919

20+
const kubeSystemNamespace = "kube-system"
21+
2022
// InformerProvider defines a function type that returns a new SharedInformer. It is used to
2123
// allow passing custom shared informers to the watch client.
2224
type InformerProvider func(
@@ -73,6 +75,27 @@ func informerWatchFuncWithSelectors(client kubernetes.Interface, namespace strin
7375
}
7476
}
7577

78+
// newKubeSystemSharedInformer watches only kube-system namespace
79+
func newKubeSystemSharedInformer(
80+
client kubernetes.Interface,
81+
) cache.SharedInformer {
82+
informer := cache.NewSharedInformer(
83+
&cache.ListWatch{
84+
ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
85+
opts.FieldSelector = fields.OneTermEqualSelector("metadata.name", kubeSystemNamespace).String()
86+
return client.CoreV1().Namespaces().List(context.Background(), opts)
87+
},
88+
WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
89+
opts.FieldSelector = fields.OneTermEqualSelector("metadata.name", kubeSystemNamespace).String()
90+
return client.CoreV1().Namespaces().Watch(context.Background(), opts)
91+
},
92+
},
93+
&api_v1.Namespace{},
94+
watchSyncPeriod,
95+
)
96+
return informer
97+
}
98+
7699
func newNamespaceSharedInformer(
77100
client kubernetes.Interface,
78101
) cache.SharedInformer {

processor/k8sattributesprocessor/internal/kube/informer_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ func Test_newSharedNamespaceInformer(t *testing.T) {
3333
assert.NotNil(t, informer)
3434
}
3535

36+
func Test_newKubeSystemSharedInformer(t *testing.T) {
37+
client, err := newFakeAPIClientset(k8sconfig.APIConfig{})
38+
require.NoError(t, err)
39+
informer := newKubeSystemSharedInformer(client)
40+
assert.NotNil(t, informer)
41+
}
42+
3643
func Test_informerListFuncWithSelectors(t *testing.T) {
3744
ls, fs, err := selectorsFromFilters(Filters{
3845
Fields: []FieldFilter{

processor/k8sattributesprocessor/internal/kube/kube.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
tagNodeName = "k8s.node.name"
2323
tagStartTime = "k8s.pod.start_time"
2424
tagHostName = "k8s.pod.hostname"
25+
tagClusterUID = "k8s.cluster.uid"
2526
// MetadataFromPod is used to specify to extract metadata/labels/annotations from pod
2627
MetadataFromPod = "pod"
2728
// MetadataFromNamespace is used to specify to extract metadata/labels/annotations from namespace
@@ -203,6 +204,7 @@ type ExtractionRules struct {
203204
ContainerID bool
204205
ContainerImageName bool
205206
ContainerImageTag bool
207+
ClusterUID bool
206208

207209
Annotations []FieldExtractionRule
208210
Labels []FieldExtractionRule

processor/k8sattributesprocessor/internal/metadata/generated_config.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)