Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: processor/k8sattributes

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Support extracting deployment name purely from the owner reference"

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [42530]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
22 changes: 20 additions & 2 deletions processor/k8sattributesprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,28 @@ The processor can be configured to set the
[recommended resource attributes](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/):

- `otel_annotations` will translate `resource.opentelemetry.io/foo` to the `foo` resource attribute, etc.
- `deployment_name_from_replicaset` allows extracting deployment name from replicaset name by trimming pod template hash. This will disable watching for replicaset resources, which can be useful in environments with limited RBAC permissions as the processor will not need `get`, `watch`, and `list` permissions for `replicasets`. It also reduces memory consumption of the processor. When enabled, this feature works automatically with the existing deployment name extraction. Take the following ownerReference of a pod managed by deployment for example:

```yaml
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: opentelemetry-collector-6c45f8d6f6
uid: ee75293d-14ec-42a0-9548-a768d9e07c48
```

The Extracted deployment name is: `opentelemetry-collector`.

> Please note, if your pods are managed by a replicaset but not by a deployment the `k8s.deployment.name` will be set incorrectly. For example, if the replicaset is named `opentelemetry-collector-6c45f8d6f6`, the feature will still set the deployment name of the pod to `opentelemetry-collector` because it skips watching for the deployment and has no context if the pod is managed by a deployment or a replicaset.

Example:

```yaml
extract:
otel_annotations: true
deployment_name_from_replicaset: true
metadata:
- service.namespace
- service.name
Expand Down Expand Up @@ -325,7 +343,7 @@ k8sattributes:

## Cluster-scoped RBAC

If you'd like to set up the k8sattributesprocessor to receive telemetry from across namespaces, it will need `get`, `watch` and `list` permissions on both `pods` and `namespaces` resources, for all namespaces and pods included in the configured filters. Additionally, when using `k8s.deployment.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.
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 (unless `deployment_name_from_replicaset` is enabled). 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.

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):

Expand Down Expand Up @@ -375,7 +393,7 @@ k8sattributes:
filter:
namespace: <WORKLOAD_NAMESPACE>
```
With the namespace filter set, the processor will only look up pods and replicasets in the selected namespace. Note that with just a role binding, the processor cannot query metadata such as labels and annotations from k8s `nodes` and `namespaces` which are cluster-scoped objects. This also means that the processor cannot set the value for `k8s.cluster.uid` attribute if enabled, since the `k8s.cluster.uid` attribute is set to the uid of the namespace `kube-system` which is not queryable with namespaced rbac.
With the namespace filter set, the processor will only look up pods and replicasets (if `deployment_name_from_replicaset` is not enabled) in the selected namespace. Note that with just a role binding, the processor cannot query metadata such as labels and annotations from k8s `nodes` and `namespaces` which are cluster-scoped objects. This also means that the processor cannot set the value for `k8s.cluster.uid` attribute if enabled, since the `k8s.cluster.uid` attribute is set to the uid of the namespace `kube-system` which is not queryable with namespaced rbac.

Please note, when extracting the workload related attributes, these workloads need to be present in the `Role` with the correct permissions. For example, an extraction of `k8s.deployment.label.*` attributes, `deployments` need to be present in `Role`.

Expand Down
4 changes: 4 additions & 0 deletions processor/k8sattributesprocessor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ type ExtractConfig struct {
// OtelAnnotations extracts all pod annotations with the prefix "resource.opentelemetry.io" as resource attributes
// E.g. "resource.opentelemetry.io/foo" becomes "foo"
OtelAnnotations bool `mapstructure:"otel_annotations"`

// DeploymentNameFromReplicaSet allows extracting deployment name from replicaset name by trimming pod template hash.
// This will disable watching for replicaset resources.
DeploymentNameFromReplicaSet bool `mapstructure:"deployment_name_from_replicaset"`
}

// FieldExtractConfig allows specifying an extraction rule to extract a resource attribute from pod (or namespace)
Expand Down
12 changes: 12 additions & 0 deletions processor/k8sattributesprocessor/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ func TestLoadConfig(t *testing.T) {
WaitForMetadataTimeout: 10 * time.Second,
},
},
{
id: component.NewIDWithName(metadata.Type, "deployment_name_from_replicaset"),
expected: &Config{
APIConfig: k8sconfig.APIConfig{AuthType: k8sconfig.AuthTypeServiceAccount},
Extract: ExtractConfig{
Metadata: enabledAttributes(),
DeploymentNameFromReplicaSet: true,
},
Exclude: defaultExcludes,
WaitForMetadataTimeout: 10 * time.Second,
},
},
{
id: component.NewIDWithName(metadata.Type, "too_many_sources"),
},
Expand Down
1 change: 1 addition & 0 deletions processor/k8sattributesprocessor/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ func createProcessorOpts(cfg component.Config) []option {
withExtractLabels(oCfg.Extract.Labels...),
withExtractAnnotations(oCfg.Extract.Annotations...),
withOtelAnnotations(oCfg.Extract.OtelAnnotations),
withDeploymentNameFromReplicaSet(oCfg.Extract.DeploymentNameFromReplicaSet),
// filters
withFilterNode(oCfg.Filter.Node, oCfg.Filter.NodeFromEnvVar),
withFilterNamespace(oCfg.Filter.Namespace),
Expand Down
56 changes: 44 additions & 12 deletions processor/k8sattributesprocessor/internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ var rRegex = regexp.MustCompile(`^(.*)-[0-9a-zA-Z]+$`)
// format: [cronjob-name]-[time-hash-int]
var cronJobRegex = regexp.MustCompile(`^(.*)-\d+$`)

// Extract Deployment name from the ReplicaSet name. Deployment name is created using
// format: [deployment-name]-[hash]
var deploymentHashSuffixPattern = regexp.MustCompile(`^[a-z0-9]{10}$`)

var errCannotRetrieveImage = errors.New("cannot retrieve image name")

type InformersFactoryList struct {
Expand Down Expand Up @@ -291,7 +295,9 @@ func (c *WatchClient) Start() error {
synced := make([]cache.InformerSynced, 0)
// start the replicaSet informer first, as the replica sets need to be
// present at the time the pods are handled, to correctly establish the connection between pods and deployments
if c.Rules.DeploymentName || c.Rules.DeploymentUID {
// The replicaset informer is needed to get the deployment UID.
// It is also needed to get the deployment name if the feature gate is not enabled.
if c.Rules.DeploymentUID || (c.Rules.DeploymentName && !c.Rules.DeploymentNameFromReplicaSet) {
reg, err := c.replicasetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.handleReplicaSetAdd,
UpdateFunc: c.handleReplicaSetUpdate,
Expand Down Expand Up @@ -829,22 +835,25 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
tags[string(conventions.ServiceNameKey)] = ref.Name
}
if c.Rules.DeploymentName || c.Rules.ServiceName {
if replicaset, ok := c.GetReplicaSet(string(ref.UID)); ok {
name := replicaset.Deployment.Name
if name != "" {
if c.Rules.DeploymentName {
tags[string(conventions.K8SDeploymentNameKey)] = name
}
if c.Rules.ServiceName {
// deployment name wins over replicaset name
tags[string(conventions.ServiceNameKey)] = name
}
var deploymentName string
if c.Rules.DeploymentNameFromReplicaSet {
deploymentName = extractDeploymentNameFromReplicaSet(ref.Name)
} else if replicaset, ok := c.GetReplicaSet(string(ref.UID)); ok {
deploymentName = replicaset.Deployment.Name
}
if deploymentName != "" {
if c.Rules.DeploymentName {
tags[string(conventions.K8SDeploymentNameKey)] = deploymentName
}
if c.Rules.ServiceName {
// deployment name wins over replicaset name
tags[string(conventions.ServiceNameKey)] = deploymentName
}
}
}
if c.Rules.DeploymentUID {
if replicaset, ok := c.GetReplicaSet(string(ref.UID)); ok {
if replicaset.Deployment.Name != "" {
if replicaset.Deployment.UID != "" {
tags[string(conventions.K8SDeploymentUIDKey)] = replicaset.Deployment.UID
}
}
Expand Down Expand Up @@ -1847,3 +1856,26 @@ func automaticServiceInstanceID(pod *api_v1.Pod, containerName string) string {
resNames := []string{pod.Namespace, pod.Name, containerName}
return strings.Join(resNames, ".")
}

// extractDeploymentNameFromReplicaSet attempts to extract deployment name from replicaset name
// by trimming the pod template hash suffix. ReplicaSets created by Deployments follow the pattern:
// <deployment-name>-<pod-template-hash> where pod-template-hash is a 10-character alphanumeric string.
func extractDeploymentNameFromReplicaSet(replicasetName string) string {
if replicasetName == "" {
return ""
}

parts := strings.Split(replicasetName, "-")
if len(parts) < 2 {
return ""
}

// Check if the last part is a valid 10-character alphanumeric hash using the pre-compiled regex.
lastPart := parts[len(parts)-1]
if deploymentHashSuffixPattern.MatchString(lastPart) {
// Return everything except the last part (the hash), joined back by hyphens.
return strings.Join(parts[:len(parts)-1], "-")
}

return ""
}
Loading