Skip to content

Commit 32981f5

Browse files
authored
[processor/k8sattributesprocessor] Add support for missing k8s.cronjob.uid (open-telemetry#42641)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description This PR adds support to expose `k8s.cronjob.uid` as resource metadata when a `Job` is owned by a `CronJob`. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes open-telemetry#42557 <!--Describe what testing was performed and which tests were added.--> #### Testing Local tested with `telemetrygen` and is working as expected. ``` [pod/k8sevents-receiver-opentelemetry-collector-6fd9966559-brlb6/opentelemetry-collector] {"level":"debug","ts":"2025-09-11T16:29:11.588Z","caller":"[email protected]/processor.go:159","msg":"getting the pod","resource":{"service.instance.id":"9631e38b-aec3-439f-8178-d96fc8368e1e","service.name":"otelcontribcol","service.version":"0.135.0-dev"},"otelcol.component.id":"k8sattributes","otelcol.component.kind":"processor","otelcol.pipeline.id":"traces","otelcol.signal":"traces","pod":{"Name":"otel-log-cronjob-29293469-lw97x","Address":"10.244.0.70","PodUID":"7960681c-5a24-4287-8bea-e2cf506500ee","Attributes":{"k8s.cronjob.name":"otel-log-cronjob","k8s.cronjob.uid":"082b1c42-e393-46bc-9d51-b20a3700d1ab","k8s.job.name":"otel-log-cronjob-29293469","k8s.job.uid":"fbd853b8-7f63-44d8-ace1-8b48c89e3041"},"StartTime":"2025-09-11T16:29:00Z","Ignore":false,"Namespace":"default","NodeName":"","DeploymentUID":"","StatefulSetUID":"","DaemonSetUID":"","JobUID":"fbd853b8-7f63-44d8-ace1-8b48c89e3041","HostNetwork":false,"Containers":{"ByID":null,"ByName":null},"DeletedAt":"0001-01-01T00:00:00Z"}} [pod/k8sevents-receiver-opentelemetry-collector-6fd9966559-brlb6/opentelemetry-collector] {"level":"info","ts":"2025-09-11T16:29:11.588Z","msg":"Traces","resource":{"service.instance.id":"9631e38b-aec3-439f-8178-d96fc8368e1e","service.name":"otelcontribcol","service.version":"0.135.0-dev"},"otelcol.component.id":"debug","otelcol.component.kind":"exporter","otelcol.signal":"traces","resource spans":1,"spans":2} [pod/k8sevents-receiver-opentelemetry-collector-6fd9966559-brlb6/opentelemetry-collector] {"level":"info","ts":"2025-09-11T16:29:11.588Z","msg":"ResourceSpans #0\nResource SchemaURL: https://opentelemetry.io/schemas/1.4.0\nResource attributes:\n -> k8s.container.name: Str(telemetrygen)\n -> service.name: Str(telemetrygen)\n -> k8s.pod.ip: Str(10.244.0.70)\n -> k8s.cronjob.name: Str(otel-log-cronjob)\n -> k8s.cronjob.uid: Str(082b1c42-e393-46bc-9d51-b20a3700d1ab)\n -> k8s.job.uid: Str(fbd853b8-7f63-44d8-ace1-8b48c89e3041)\n -> k8s.job.name: Str(otel-log-cronjob-29293469)\nScopeSpans #0\nScopeSpans SchemaURL: \nInstrumentationScope telemetrygen \nSpan #0\n Trace ID : 3c7381c14a37814676b00a7d961cb219\n Parent ID : 4f8780d5148a9c1c\n ID : 17e9da9533dc93ca\n Name : okey-dokey-0\n Kind : Server\n Start time : 2025-09-11 16:29:09.583785469 +0000 UTC\n End time : 2025-09-11 16:29:09.583908469 +0000 UTC\n Status code : Unset\n Status message : \nAttributes:\n -> net.peer.ip: Str(1.2.3.4)\n -> peer.service: Str(telemetrygen-client)\nSpan #1\n Trace ID : 3c7381c14a37814676b00a7d961cb219\n Parent ID : \n ID : 4f8780d5148a9c1c\n Name : lets-go\n Kind : Client\n Start time : 2025-09-11 16:29:09.583785469 +0000 UTC\n End time : 2025-09-11 16:29:09.583908469 +0000 UTC\n Status code : Unset\n Status message : \nAttributes:\n -> net.peer.ip: Str(1.2.3.4)\n -> peer.service: Str(telemetrygen-server)\n","resource":{"service.instance.id":"9631e38b-aec3-439f-8178-d96fc8368e1e","service.name":"otelcontribcol","service.version":"0.135.0-dev"},"otelcol.component.id":"debug","otelcol.component.kind":"exporter","otelcol.signal":"traces"} ``` Added also the tests to guarantee the proper functionality. --------- Signed-off-by: Paulo Dias <[email protected]>
1 parent 8cb8ef6 commit 32981f5

File tree

20 files changed

+636
-21
lines changed

20 files changed

+636
-21
lines changed

.chloggen/feat_42557.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: "processor/k8sattributesprocessor"
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Add support for k8s.cronjob.uid attribute in k8sattributesprocessor"
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: [42557]
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]

pkg/xk8stest/k8s_collector.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package xk8stest // import "github.com/open-telemetry/opentelemetry-collector-co
55

66
import (
77
"bytes"
8+
"maps"
89
"os"
910
"path/filepath"
1011
"testing"
@@ -39,9 +40,7 @@ func CreateCollectorObjects(t *testing.T, client *K8sClient, testID, manifestsDi
3940
"HostEndpoint": host,
4041
"TestID": testID,
4142
}
42-
for key, value := range templateValues {
43-
defaultTemplateValues[key] = value
44-
}
43+
maps.Copy(defaultTemplateValues, templateValues)
4544
require.NoError(t, tmpl.Execute(manifest, defaultTemplateValues))
4645
obj, err := CreateObject(client, manifest.Bytes())
4746
require.NoErrorf(t, err, "failed to create collector object from manifest %s", manifestFile.Name())

pkg/xk8stest/k8s_telemetrygen.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package xk8stest // import "github.com/open-telemetry/opentelemetry-collector-co
55

66
import (
77
"bytes"
8+
"fmt"
89
"os"
910
"path/filepath"
1011
"testing"
@@ -31,6 +32,78 @@ type TelemetrygenCreateOpts struct {
3132
DataTypes []string
3233
}
3334

35+
// getPodLabelSelectors returns labels used to select pods created by the workload.
36+
// - Deployment/StatefulSet/DaemonSet: spec.selector.matchLabels (fallback to template.metadata.labels)
37+
// - Job: spec.template.metadata.labels
38+
// - CronJob: spec.jobTemplate.spec.template.metadata.labels
39+
func getPodLabelSelectors(obj *unstructured.Unstructured) (map[string]any, error) {
40+
o := obj.Object
41+
spec, ok := o["spec"].(map[string]any)
42+
if !ok || spec == nil {
43+
return nil, fmt.Errorf("%s/%s missing spec", obj.GetKind(), obj.GetName())
44+
}
45+
46+
switch obj.GetKind() {
47+
case "Deployment", "StatefulSet", "DaemonSet":
48+
if sel, ok := spec["selector"].(map[string]any); ok && sel != nil {
49+
if ml, ok := sel["matchLabels"].(map[string]any); ok && ml != nil {
50+
return ml, nil
51+
}
52+
}
53+
// fallback — uncommon but robust
54+
if tmpl, ok := spec["template"].(map[string]any); ok && tmpl != nil {
55+
if meta, ok := tmpl["metadata"].(map[string]any); ok && meta != nil {
56+
if ml, ok := meta["labels"].(map[string]any); ok && ml != nil {
57+
return ml, nil
58+
}
59+
}
60+
}
61+
return nil, fmt.Errorf("%s/%s missing selector.matchLabels and template.metadata.labels", obj.GetKind(), obj.GetName())
62+
63+
case "Job":
64+
if tmpl, ok := spec["template"].(map[string]any); ok && tmpl != nil {
65+
if meta, ok := tmpl["metadata"].(map[string]any); ok && meta != nil {
66+
if ml, ok := meta["labels"].(map[string]any); ok && ml != nil {
67+
return ml, nil
68+
}
69+
}
70+
}
71+
// last resort if API server already defaulted it
72+
if sel, ok := spec["selector"].(map[string]any); ok && sel != nil {
73+
if ml, ok := sel["matchLabels"].(map[string]any); ok && ml != nil {
74+
return ml, nil
75+
}
76+
}
77+
return nil, fmt.Errorf("Job/%s missing template.metadata.labels (and selector.matchLabels)", obj.GetName())
78+
79+
case "CronJob":
80+
jt, ok := spec["jobTemplate"].(map[string]any)
81+
if !ok || jt == nil {
82+
return nil, fmt.Errorf("CronJob/%s missing spec.jobTemplate", obj.GetName())
83+
}
84+
jts, ok := jt["spec"].(map[string]any)
85+
if !ok || jts == nil {
86+
return nil, fmt.Errorf("CronJob/%s missing spec.jobTemplate.spec", obj.GetName())
87+
}
88+
tmpl, ok := jts["template"].(map[string]any)
89+
if !ok || tmpl == nil {
90+
return nil, fmt.Errorf("CronJob/%s missing spec.jobTemplate.spec.template", obj.GetName())
91+
}
92+
meta, ok := tmpl["metadata"].(map[string]any)
93+
if !ok || meta == nil {
94+
return nil, fmt.Errorf("CronJob/%s missing spec.jobTemplate.spec.template.metadata", obj.GetName())
95+
}
96+
ml, ok := meta["labels"].(map[string]any)
97+
if !ok || ml == nil {
98+
return nil, fmt.Errorf("CronJob/%s missing spec.jobTemplate.spec.template.metadata.labels", obj.GetName())
99+
}
100+
return ml, nil
101+
102+
default:
103+
return nil, fmt.Errorf("unsupported kind %q", obj.GetKind())
104+
}
105+
}
106+
34107
func CreateTelemetryGenObjects(t *testing.T, client *K8sClient, createOpts *TelemetrygenCreateOpts) ([]*unstructured.Unstructured, []*TelemetrygenObjInfo) {
35108
telemetrygenObjInfos := make([]*TelemetrygenObjInfo, 0)
36109
manifestFiles, err := os.ReadDir(createOpts.ManifestsDir)
@@ -48,10 +121,13 @@ func CreateTelemetryGenObjects(t *testing.T, client *K8sClient, createOpts *Tele
48121
}))
49122
obj, err := CreateObject(client, manifest.Bytes())
50123
require.NoErrorf(t, err, "failed to create telemetrygen object from manifest %s", manifestFile.Name())
51-
selector := obj.Object["spec"].(map[string]any)["selector"]
124+
125+
podLabels, err := getPodLabelSelectors(obj)
126+
require.NoErrorf(t, err, "failed to extract pod label selectors for %s %s", obj.GetKind(), obj.GetName())
127+
52128
telemetrygenObjInfos = append(telemetrygenObjInfos, &TelemetrygenObjInfo{
53129
Namespace: obj.GetNamespace(),
54-
PodLabelSelectors: selector.(map[string]any)["matchLabels"].(map[string]any),
130+
PodLabelSelectors: podLabels,
55131
DataType: dataType,
56132
Workload: obj.GetKind(),
57133
})

processor/k8sattributesprocessor/README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The processor stores the list of running pods and the associated metadata. When
3232
to the pod from where the datapoint originated, so we can add the relevant pod metadata to the datapoint. By default, it associates the incoming connection IP
3333
to the Pod IP. But for cases where this approach doesn't work (sending through a proxy, etc.), a custom association rule can be specified.
3434

35-
Each association is specified as a list of sources of associations. The maximum number of sources within an association is 4.
35+
Each association is specified as a list of sources of associations. The maximum number of sources within an association is 4.
3636
A source is a rule that matches metadata from the datapoint to pod metadata.
3737
In order to get an association applied, all the sources specified need to match.
3838

@@ -63,16 +63,16 @@ If Pod association rules are not configured, resources are associated with metad
6363
6464
Which metadata to collect is determined by `metadata` configuration that defines list of resource attributes
6565
to be added. Items in the list called exactly the same as the resource attributes that will be added.
66-
The following attributes are added by default:
66+
The following attributes are added by default:
6767
- k8s.namespace.name
6868
- k8s.pod.name
6969
- k8s.pod.uid
7070
- k8s.pod.start_time
7171
- k8s.deployment.name
7272
- k8s.node.name
7373

74-
These attributes are also available for the use within association rules by default.
75-
The `metadata` section can also be extended with additional attributes which, if present in the `metadata` section,
74+
These attributes are also available for the use within association rules by default.
75+
The `metadata` section can also be extended with additional attributes which, if present in the `metadata` section,
7676
are then also available for the use within association rules. Available attributes are:
7777
- k8s.namespace.name
7878
- k8s.pod.name
@@ -100,7 +100,7 @@ are then also available for the use within association rules. Available attribut
100100
- [service.instance.id](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#how-serviceinstanceid-should-be-calculated)(cannot be used for source rules in the pod_association)
101101
- Any tags extracted from the pod labels and annotations, as described in [extracting attributes from pod labels and annotations](#extracting-attributes-from-pod-labels-and-annotations)
102102

103-
Not all the attributes are guaranteed to be added. Only attribute names from `metadata` should be used for
103+
Not all the attributes are guaranteed to be added. Only attribute names from `metadata` should be used for
104104
pod_association's `resource_attribute`, because empty or non-existing values will be ignored.
105105

106106
Additional container level attributes can be extracted. If a pod contains more than one container,
@@ -204,7 +204,7 @@ the processor associates the received trace to the pod, based on the connection
204204
"k8s.pod.name": "telemetrygen-pod",
205205
"k8s.pod.uid": "038e2267-b473-489b-b48c-46bafdb852eb",
206206
"container.image.name": "telemetrygen",
207-
"container.image.tag": "0.112.0",
207+
"container.image.tag": "0.112.0",
208208
"container.image.repo_digests": ["ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:b248ef911f93ae27cbbc85056d1ffacc87fd941bbdc2ffd951b6df8df72b8096"]
209209
}
210210
}
@@ -262,16 +262,16 @@ extract:
262262
from: node
263263
```
264264
265-
## Configuring recommended resource attributes
265+
## Configuring recommended resource attributes
266266
267-
The processor can be configured to set the
267+
The processor can be configured to set the
268268
[recommended resource attributes](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/):
269269
270270
- `otel_annotations` will translate `resource.opentelemetry.io/foo` to the `foo` resource attribute, etc.
271271

272272
```yaml
273273
extract:
274-
otel_annotations: true
274+
otel_annotations: true
275275
metadata:
276276
- service.namespace
277277
- service.name
@@ -306,7 +306,7 @@ k8sattributes:
306306
- tag_name: app.label.component
307307
key: app.kubernetes.io/component
308308
from: pod
309-
otel_annotations: true
309+
otel_annotations: true
310310
pod_association:
311311
- sources:
312312
# 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.
@@ -325,7 +325,7 @@ k8sattributes:
325325

326326
## Cluster-scoped RBAC
327327

328-
If you'd like to set up the k8sattributesprocessor to receive telemetry from across namespaces, it will need `get`, `watch` and `list` permissions on both `pods` and `namespaces` resources, for all namespaces and pods included in the configured filters. Additionally, when using `k8s.deployment.name` (which is enabled by default) or `k8s.deployment.uid` the processor also needs `get`, `watch` and `list` permissions for `replicasets` resources. When using `k8s.node.uid` or extracting metadata from `node`, the processor needs `get`, `watch` and `list` permissions for `nodes` resources.
328+
If you'd like to set up the k8sattributesprocessor to receive telemetry from across namespaces, it will need `get`, `watch` and `list` permissions on both `pods` and `namespaces` resources, for all namespaces and pods included in the configured filters. Additionally, when using `k8s.deployment.name` (which is enabled by default) or `k8s.deployment.uid` the processor also needs `get`, `watch` and `list` permissions for `replicasets` resources. When using `k8s.node.uid` or extracting metadata from `node`, the processor needs `get`, `watch` and `list` permissions for `nodes` resources. When using `k8s.cronjob.uid` the processor also needs `get`, `watch` and `list` permissions for `jobs` resources.
329329

330330
Here is an example of a `ClusterRole` to give a `ServiceAccount` the necessary permissions for all pods, nodes, and namespaces in the cluster (replace `<OTEL_COL_NAMESPACE>` with a namespace where collector is deployed):
331331

processor/k8sattributesprocessor/client_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type fakeClient struct {
3131
Deployments map[string]*kube.Deployment
3232
StatefulSets map[string]*kube.StatefulSet
3333
DaemonSets map[string]*kube.DaemonSet
34+
ReplicaSets map[string]*kube.ReplicaSet
3435
Jobs map[string]*kube.Job
3536
StopCh chan struct{}
3637
}
@@ -90,6 +91,11 @@ func (f *fakeClient) GetDaemonSet(daemonsetUID string) (*kube.DaemonSet, bool) {
9091
return s, ok
9192
}
9293

94+
func (f *fakeClient) GetReplicaSet(replicaSetUID string) (*kube.ReplicaSet, bool) {
95+
rs, ok := f.ReplicaSets[replicaSetUID]
96+
return rs, ok
97+
}
98+
9399
func (f *fakeClient) GetJob(jobUID string) (*kube.Job, bool) {
94100
j, ok := f.Jobs[jobUID]
95101
return j, ok

processor/k8sattributesprocessor/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func (cfg *Config) Validate() error {
9797
string(conventions.K8SDaemonSetNameKey), string(conventions.K8SDaemonSetUIDKey),
9898
string(conventions.K8SStatefulSetNameKey), string(conventions.K8SStatefulSetUIDKey),
9999
string(conventions.K8SJobNameKey), string(conventions.K8SJobUIDKey),
100-
string(conventions.K8SCronJobNameKey),
100+
string(conventions.K8SCronJobNameKey), string(conventions.K8SCronJobUIDKey),
101101
string(conventions.K8SNodeNameKey), string(conventions.K8SNodeUIDKey),
102102
string(conventions.K8SContainerNameKey), string(conventions.ContainerIDKey),
103103
string(conventions.ContainerImageNameKey), string(conventions.ContainerImageTagKey),
@@ -139,7 +139,8 @@ type ExtractConfig struct {
139139
// k8s.node.name, k8s.namespace.name, k8s.pod.start_time,
140140
// k8s.replicaset.name, k8s.replicaset.uid,
141141
// k8s.daemonset.name, k8s.daemonset.uid,
142-
// k8s.job.name, k8s.job.uid, k8s.cronjob.name,
142+
// k8s.job.name, k8s.job.uid,
143+
// k8s.cronjob.name, k8s.cronjob.uid,
143144
// k8s.statefulset.name, k8s.statefulset.uid,
144145
// k8s.container.name, container.id, container.image.name,
145146
// container.image.tag, container.image.repo_digests

processor/k8sattributesprocessor/documentation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
| k8s.cluster.uid | Gives cluster uid identified with kube-system namespace | Any Str | false |
1414
| k8s.container.name | The name of the Container in a Pod template. Requires container.id. | Any Str | false |
1515
| k8s.cronjob.name | The name of the CronJob. | Any Str | false |
16+
| k8s.cronjob.uid | The uid of the CronJob. | Any Str | false |
1617
| k8s.daemonset.name | The name of the DaemonSet. | Any Str | false |
1718
| k8s.daemonset.uid | The UID of the DaemonSet. | Any Str | false |
1819
| k8s.deployment.name | The name of the Deployment. | Any Str | true |

0 commit comments

Comments
 (0)