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
Next Next commit
[processor/k8sattributes] introduce semconv compaint feature gate pair
Signed-off-by: odubajDT <ondrej.dubaj@dynatrace.com>
  • Loading branch information
odubajDT committed Jan 29, 2026
commit 8cd1c344e0d6d65e1cf714e66b005f0e46f56a8a
33 changes: 33 additions & 0 deletions .chloggen/k8sattributes-semconv-gates.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Use this changelog template to create an entry for release notes.

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

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

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Introduce semantic conventions compliant feature gate pair for k8sattributes processor

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

# (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: |
- Added `semconv.k8s.k8sattributes.enableStable` feature gate to enable stable semantic convention attributes (singular form: `k8s.<workload>.label.<key>` and `k8s.<workload>.annotation.<key>`)
- Added `semconv.k8s.k8sattributes.disableLegacy` feature gate to disable legacy non-compliant attributes (plural form: `k8s.<workload>.labels.<key>` and `k8s.<workload>.annotations.<key>`)
- Both feature gates are in `alpha` stage and disabled by default
- The processor now validates that legacy attributes cannot be disabled without enabling stable attributes
- Deprecated `k8sattr.labelsAnnotationsSingular.allow` feature gate in favor of the new semconv-compliant gates (will be removed in v0.150.0)
- During migration period, both legacy and stable attributes can coexist when `enableStable` is enabled but `disableLegacy` is not

# 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: []
92 changes: 59 additions & 33 deletions processor/k8sattributesprocessor/internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/distribution/reference"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/featuregate"
"go.opentelemetry.io/otel/attribute"
conventions "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.uber.org/zap"
Expand Down Expand Up @@ -64,6 +65,21 @@ const (
K8sJobAnnotation = "k8s.job.annotation.%s"
)

var (
EnableStableAttributes = featuregate.GlobalRegistry().MustRegister(
"semconv.k8s.k8sattributes.enableStable",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, semconv stable attributes are enabled."),
featuregate.WithRegisterFromVersion("v0.144.0"),
)
DisableLegacyAttributes = featuregate.GlobalRegistry().MustRegister(
"semconv.k8s.k8sattributes.disableLegacy",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, semconv legacy attributes are disabled."),
featuregate.WithRegisterFromVersion("v0.144.0"),
)
)

// WatchClient is the main interface provided by this package to a kubernetes cluster.
type WatchClient struct {
m sync.RWMutex
Expand Down Expand Up @@ -918,18 +934,25 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
}
}

formatterLabel := K8sPodLabelsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterLabel = K8sPodLabelKey
}
enableStable := EnableStableAttributes.IsEnabled()
disableLegacy := DisableLegacyAttributes.IsEnabled()

for _, r := range c.Rules.Labels {
r.extractFromPodMetadata(pod.Labels, tags, formatterLabel)
if !disableLegacy {
r.extractFromPodMetadata(pod.Labels, tags, K8sPodLabelsKey)
}
if enableStable {
r.extractFromPodMetadata(pod.Labels, tags, K8sPodLabelKey)
}
}

formatterAnnotation := K8sPodAnnotationsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterAnnotation = K8sPodAnnotationKey
for _, r := range c.Rules.Annotations {
if !disableLegacy {
r.extractFromPodMetadata(pod.Annotations, tags, K8sPodAnnotationsKey)
}
if enableStable {
r.extractFromPodMetadata(pod.Annotations, tags, K8sPodAnnotationKey)
}
}

if c.Rules.ServiceName {
Expand All @@ -942,9 +965,6 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
copyLabel(pod, tags, "app.kubernetes.io/version", conventions.ServiceVersionKey)
}

for _, r := range c.Rules.Annotations {
r.extractFromPodMetadata(pod.Annotations, tags, formatterAnnotation)
}
return tags
}

Expand Down Expand Up @@ -1163,22 +1183,25 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain
func (c *WatchClient) extractNamespaceAttributes(namespace *api_v1.Namespace) map[string]string {
tags := map[string]string{}

formatterLabel := K8sNamespaceLabelsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterLabel = K8sNamespaceLabelKey
}
enableStable := EnableStableAttributes.IsEnabled()
disableLegacy := DisableLegacyAttributes.IsEnabled()

for _, r := range c.Rules.Labels {
r.extractFromNamespaceMetadata(namespace.Labels, tags, formatterLabel)
}

formatterAnnotation := K8sNamespaceAnnotationsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterAnnotation = K8sNamespaceAnnotationKey
if !disableLegacy {
r.extractFromNamespaceMetadata(namespace.Labels, tags, K8sNamespaceLabelsKey)
}
if enableStable {
r.extractFromNamespaceMetadata(namespace.Labels, tags, K8sNamespaceLabelKey)
}
}

for _, r := range c.Rules.Annotations {
r.extractFromNamespaceMetadata(namespace.Annotations, tags, formatterAnnotation)
if !disableLegacy {
r.extractFromNamespaceMetadata(namespace.Annotations, tags, K8sNamespaceAnnotationsKey)
}
if enableStable {
r.extractFromNamespaceMetadata(namespace.Annotations, tags, K8sNamespaceAnnotationKey)
}
}

return tags
Expand All @@ -1187,22 +1210,25 @@ func (c *WatchClient) extractNamespaceAttributes(namespace *api_v1.Namespace) ma
func (c *WatchClient) extractNodeAttributes(node *api_v1.Node) map[string]string {
tags := map[string]string{}

formatterLabel := K8sNodeLabelsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterLabel = K8sNodeLabelKey
}
enableStable := EnableStableAttributes.IsEnabled()
disableLegacy := DisableLegacyAttributes.IsEnabled()

for _, r := range c.Rules.Labels {
r.extractFromNodeMetadata(node.Labels, tags, formatterLabel)
}

formatterAnnotation := K8sNodeAnnotationsKey
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
formatterAnnotation = K8sNodeAnnotationKey
if !disableLegacy {
r.extractFromNodeMetadata(node.Labels, tags, K8sNodeLabelsKey)
}
if enableStable {
r.extractFromNodeMetadata(node.Labels, tags, K8sNodeLabelKey)
}
}

for _, r := range c.Rules.Annotations {
r.extractFromNodeMetadata(node.Annotations, tags, formatterAnnotation)
if !disableLegacy {
r.extractFromNodeMetadata(node.Annotations, tags, K8sNodeAnnotationsKey)
}
if enableStable {
r.extractFromNodeMetadata(node.Annotations, tags, K8sNodeAnnotationKey)
}
}
return tags
}
Expand Down
19 changes: 12 additions & 7 deletions processor/k8sattributesprocessor/internal/kube/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"k8s.io/client-go/tools/cache"

"github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig"
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor/internal/metadata"
)

func newFakeAPIClientset(_ k8sconfig.APIConfig) (kubernetes.Interface, error) {
Expand Down Expand Up @@ -1074,9 +1073,11 @@ func TestExtractionRules(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.singularFeatureGate {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), true))
defer func() {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), false))
}()
}

Expand Down Expand Up @@ -1378,9 +1379,11 @@ func TestNamespaceExtractionRules(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.singularFeatureGate {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), true))
defer func() {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), false))
}()
}

Expand Down Expand Up @@ -1636,9 +1639,11 @@ func TestNodeExtractionRules(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.singularFeatureGate {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), true))
defer func() {
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(EnableStableAttributes.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(DisableLegacyAttributes.ID(), false))
}()
}

Expand Down
6 changes: 4 additions & 2 deletions processor/k8sattributesprocessor/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,11 @@ func extractFieldRules(fieldType string, fields ...FieldExtractConfig) ([]kube.F

if name == "" && a.Key != "" {
// name for KeyRegex case is set at extraction time/runtime, skipped here
// Use singular form when feature gate is enabled
// Use singular form when stable attributes are enabled and legacy attributes are disabled
fieldTypeName := fieldType
if metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.IsEnabled() {
enableStable := kube.EnableStableAttributes.IsEnabled()
disableLegacy := kube.DisableLegacyAttributes.IsEnabled()
if enableStable && disableLegacy {
fieldTypeName = strings.TrimSuffix(fieldType, "s")
}
name = fmt.Sprintf("k8s.%v.%v.%v", a.From, fieldTypeName, a.Key)
Expand Down
11 changes: 7 additions & 4 deletions processor/k8sattributesprocessor/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (

"github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig"
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor/internal/kube"
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor/internal/metadata"
)

func TestWithAPIConfig(t *testing.T) {
Expand Down Expand Up @@ -736,11 +735,15 @@ func Test_extractFieldRules_FeatureGate(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set feature gate state
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), tt.featureGateValue))
// Set feature gate state for stable and legacy attributes
if tt.featureGateValue {
require.NoError(t, featuregate.GlobalRegistry().Set(kube.EnableStableAttributes.ID(), true))
require.NoError(t, featuregate.GlobalRegistry().Set(kube.DisableLegacyAttributes.ID(), true))
}
defer func() {
// Reset to default
require.NoError(t, featuregate.GlobalRegistry().Set(metadata.K8sattrLabelsAnnotationsSingularAllowFeatureGate.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(kube.EnableStableAttributes.ID(), false))
require.NoError(t, featuregate.GlobalRegistry().Set(kube.DisableLegacyAttributes.ID(), false))
}()

got, err := extractFieldRules(tt.fieldType, tt.fields...)
Expand Down
7 changes: 7 additions & 0 deletions processor/k8sattributesprocessor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package k8sattributesprocessor // import "github.com/open-telemetry/opentelemetr

import (
"context"
"errors"
"fmt"
"strconv"
"time"
Expand Down Expand Up @@ -58,6 +59,12 @@ func (kp *kubernetesprocessor) initKubeClient(set component.TelemetrySettings, k
}

func (kp *kubernetesprocessor) Start(_ context.Context, host component.Host) error {
if kube.DisableLegacyAttributes.IsEnabled() && !kube.EnableStableAttributes.IsEnabled() {
err := errors.New("cannot disable legacy attributes without enabling stable attributes")
componentstatus.ReportStatus(host, componentstatus.NewFatalErrorEvent(err))
return err
}

allOptions := append(createProcessorOpts(kp.cfg), kp.options...)

for _, opt := range allOptions {
Expand Down