diff --git a/.chloggen/signalfxexporter-strip-k8s-label-prefix.yaml b/.chloggen/signalfxexporter-strip-k8s-label-prefix.yaml new file mode 100644 index 0000000000000..4e559796f30a7 --- /dev/null +++ b/.chloggen/signalfxexporter-strip-k8s-label-prefix.yaml @@ -0,0 +1,16 @@ +change_type: enhancement + +component: exporter/signalfx + +note: Add `dimension_client::strip_k8s_label_prefix` option to strip `k8s..label.` prefix from dimension property updates. + +issues: [47491] + +subtext: | + The k8s cluster receiver now emits Kubernetes resource labels in entity events with the + `k8s..label.` prefix per OTel semantic conventions (e.g. `k8s.pod.label.app`). + When `strip_k8s_label_prefix: true` (the default), the SignalFx exporter strips this prefix + when forwarding labels as dimension properties, preserving the existing SignalFx behavior (e.g. `app`). + Set `strip_k8s_label_prefix: false` to disable stripping and receive the full prefixed keys. + +change_logs: [user] diff --git a/exporter/signalfxexporter/config.go b/exporter/signalfxexporter/config.go index 90ae76afafe76..31f9b4958024e 100644 --- a/exporter/signalfxexporter/config.go +++ b/exporter/signalfxexporter/config.go @@ -163,6 +163,7 @@ type DimensionClientConfig struct { IdleConnTimeout time.Duration `mapstructure:"idle_conn_timeout"` Timeout time.Duration `mapstructure:"timeout"` DropTags bool `mapstructure:"drop_tags"` + StripK8sLabelPrefix bool `mapstructure:"strip_k8s_label_prefix"` } func (cfg *Config) getMetricTranslator(done chan struct{}) (*translation.MetricTranslator, error) { diff --git a/exporter/signalfxexporter/config.schema.yaml b/exporter/signalfxexporter/config.schema.yaml index 72081ed09b907..88fa1199fd506 100644 --- a/exporter/signalfxexporter/config.schema.yaml +++ b/exporter/signalfxexporter/config.schema.yaml @@ -18,6 +18,8 @@ $defs: send_delay: type: string format: duration + strip_k8s_label_prefix: + type: boolean timeout: type: string format: duration diff --git a/exporter/signalfxexporter/config_test.go b/exporter/signalfxexporter/config_test.go index 956c97da4abeb..167cc06adf316 100644 --- a/exporter/signalfxexporter/config_test.go +++ b/exporter/signalfxexporter/config_test.go @@ -86,6 +86,7 @@ func TestLoadConfig(t *testing.T) { IdleConnTimeout: 30 * time.Second, Timeout: 10 * time.Second, DropTags: false, + StripK8sLabelPrefix: true, }, ExcludeMetrics: nil, IncludeMetrics: nil, @@ -165,6 +166,7 @@ func TestLoadConfig(t *testing.T) { IdleConnTimeout: 2 * time.Hour, Timeout: 20 * time.Second, DropTags: false, + StripK8sLabelPrefix: true, }, DefaultProperties: map[string]string{ "foo": "bar", diff --git a/exporter/signalfxexporter/exporter.go b/exporter/signalfxexporter/exporter.go index 264b9990eaa9a..90f38a3a4ec98 100644 --- a/exporter/signalfxexporter/exporter.go +++ b/exporter/signalfxexporter/exporter.go @@ -172,6 +172,7 @@ func (se *signalfxExporter) startDimensionClient(ctx context.Context) error { IdleConnTimeout: se.config.DimensionClient.IdleConnTimeout, Timeout: se.config.DimensionClient.Timeout, DropTags: se.config.DimensionClient.DropTags, + StripK8sLabelPrefix: se.config.DimensionClient.StripK8sLabelPrefix, }) dimClient.Start() se.dimClient = dimClient diff --git a/exporter/signalfxexporter/factory.go b/exporter/signalfxexporter/factory.go index b4159130f1fb8..87197fb4f1cab 100644 --- a/exporter/signalfxexporter/factory.go +++ b/exporter/signalfxexporter/factory.go @@ -84,6 +84,7 @@ func createDefaultConfig() component.Config { MaxIdleConnsPerHost: defaultDimMaxIdleConnsPerHost, IdleConnTimeout: idleConnTimeout, Timeout: timeout, + StripK8sLabelPrefix: true, }, } } diff --git a/exporter/signalfxexporter/internal/dimensions/dimclient.go b/exporter/signalfxexporter/internal/dimensions/dimclient.go index 90e967cd17934..a350557aa939c 100644 --- a/exporter/signalfxexporter/internal/dimensions/dimclient.go +++ b/exporter/signalfxexporter/internal/dimensions/dimclient.go @@ -61,6 +61,11 @@ type DimensionClient struct { ExcludeProperties []dpfilters.PropertyFilter // dropTags specifies whether tags should be omitted or not. Default value is false. dropTags bool + // stripK8sLabelPrefix controls whether the `k8s..label.` prefix is stripped + // from Kubernetes resource label keys before sending them as dimension property updates. + // This applies to all resource types except k8s.service, which already sends labels with + // the prefix. Default is true. + stripK8sLabelPrefix bool } type queuedDimension struct { @@ -87,6 +92,7 @@ type DimensionClientOptions struct { IdleConnTimeout time.Duration Timeout time.Duration DropTags bool + StripK8sLabelPrefix bool } // NewDimensionClient returns a new client @@ -125,6 +131,7 @@ func NewDimensionClient(options DimensionClientOptions) *DimensionClient { DefaultProperties: options.DefaultProperties, ExcludeProperties: options.ExcludeProperties, dropTags: options.DropTags, + stripK8sLabelPrefix: options.StripK8sLabelPrefix, } } diff --git a/exporter/signalfxexporter/internal/dimensions/metadata.go b/exporter/signalfxexporter/internal/dimensions/metadata.go index dd69d5dc49075..018251c30dbb5 100644 --- a/exporter/signalfxexporter/internal/dimensions/metadata.go +++ b/exporter/signalfxexporter/internal/dimensions/metadata.go @@ -5,6 +5,7 @@ package dimensions // import "github.com/open-telemetry/opentelemetry-collector- import ( "fmt" + "regexp" "strings" "unicode" @@ -22,9 +23,10 @@ func getDimensionUpdateFromMetadata( defaults map[string]string, metadata metadata.MetadataUpdate, nonAlphanumericDimChars string, + stripK8sLabelPrefix bool, ) *DimensionUpdate { skipSanitization := metadata.ResourceIDKey == "k8s.service.uid" - properties, tags := getPropertiesAndTags(defaults, metadata, skipSanitization) + properties, tags := getPropertiesAndTags(defaults, metadata, skipSanitization, stripK8sLabelPrefix) return &DimensionUpdate{ Name: FilterKeyChars(metadata.ResourceIDKey, nonAlphanumericDimChars), @@ -39,14 +41,21 @@ const ( sfxK8sServicePrefix = "kubernetes_service_" ) -func sanitizeProperty(property string) string { +var oTelK8sLabelRe = regexp.MustCompile(`^k8s\.[^.]+\.label\.(.+)$`) + +func sanitizeProperty(property string, stripK8sLabelPrefix bool) string { if strings.HasPrefix(property, oTelK8sServicePrefix) { return strings.Replace(property, oTelK8sServicePrefix, sfxK8sServicePrefix, 1) } + if stripK8sLabelPrefix { + if m := oTelK8sLabelRe.FindStringSubmatch(property); m != nil { + return m[1] + } + } return property } -func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdate, skipSanitization bool) (map[string]*string, map[string]bool) { +func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdate, skipSanitization, stripK8sLabelPrefix bool) (map[string]*string, map[string]bool) { properties := map[string]*string{} tags := map[string]bool{} @@ -57,7 +66,7 @@ func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdat for label, val := range kmu.MetadataToAdd { key := label if !skipSanitization { - key = sanitizeProperty(label) + key = sanitizeProperty(label, stripK8sLabelPrefix) } if key == "" { continue @@ -74,7 +83,7 @@ func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdat for label, val := range kmu.MetadataToRemove { key := label if !skipSanitization { - key = sanitizeProperty(label) + key = sanitizeProperty(label, stripK8sLabelPrefix) } if key == "" { continue @@ -90,7 +99,7 @@ func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdat for label, val := range kmu.MetadataToUpdate { key := label if !skipSanitization { - key = sanitizeProperty(label) + key = sanitizeProperty(label, stripK8sLabelPrefix) } if key == "" { continue @@ -112,7 +121,7 @@ func getPropertiesAndTags(defaults map[string]string, kmu metadata.MetadataUpdat func (dc *DimensionClient) PushMetadata(metadata []*metadata.MetadataUpdate) error { var errs error for _, m := range metadata { - dimensionUpdate := getDimensionUpdateFromMetadata(dc.DefaultProperties, *m, dc.nonAlphanumericDimChars) + dimensionUpdate := getDimensionUpdateFromMetadata(dc.DefaultProperties, *m, dc.nonAlphanumericDimChars, dc.stripK8sLabelPrefix) if dimensionUpdate.Name == "" || dimensionUpdate.Value == "" { return fmt.Errorf("dimensionUpdate %v is missing Name or value, cannot send", dimensionUpdate) diff --git a/exporter/signalfxexporter/internal/dimensions/metadata_test.go b/exporter/signalfxexporter/internal/dimensions/metadata_test.go index 2b8d29bd69456..88b6bbdb9485d 100644 --- a/exporter/signalfxexporter/internal/dimensions/metadata_test.go +++ b/exporter/signalfxexporter/internal/dimensions/metadata_test.go @@ -13,8 +13,9 @@ import ( func TestGetDimensionUpdateFromMetadata(t *testing.T) { type args struct { - defaults map[string]string - metadata metadata.MetadataUpdate + defaults map[string]string + metadata metadata.MetadataUpdate + stripK8sLabelPrefix bool } tests := []struct { name string @@ -117,6 +118,63 @@ func TestGetDimensionUpdateFromMetadata(t *testing.T) { }, }, }, + { + "Test k8s resource label prefix is stripped", + args{ + stripK8sLabelPrefix: true, + metadata: metadata.MetadataUpdate{ + ResourceIDKey: "k8s.pod.uid", + ResourceID: "pod-123", + MetadataDelta: metadata.MetadataDelta{ + MetadataToAdd: map[string]string{ + "k8s.pod.label.app": "my-app", + "k8s.node.label.topology.kubernetes.io/zone": "", + "k8s.deployment.label.version": "v1", + }, + MetadataToRemove: map[string]string{ + "k8s.pod.label.old-label": "old-value", + }, + }, + }, + }, + &DimensionUpdate{ + Name: "k8s.pod.uid", + Value: "pod-123", + Properties: getMapToPointers(map[string]string{ + "app": "my-app", + "version": "v1", + "old-label": "", + }), + Tags: map[string]bool{ + "topology.kubernetes.io/zone": true, + }, + }, + }, + { + "Test k8s service label prefix is NOT stripped even when stripK8sLabelPrefix is true", + args{ + stripK8sLabelPrefix: true, + metadata: metadata.MetadataUpdate{ + ResourceIDKey: "k8s.service.uid", + ResourceID: "svc-123", + MetadataDelta: metadata.MetadataDelta{ + MetadataToAdd: map[string]string{ + "k8s.service.label.app": "my-app", + "k8s.service.label.version": "v1", + }, + }, + }, + }, + &DimensionUpdate{ + Name: "k8s.service.uid", + Value: "svc-123", + Properties: getMapToPointers(map[string]string{ + "k8s.service.label.app": "my-app", + "k8s.service.label.version": "v1", + }), + Tags: map[string]bool{}, + }, + }, { "Test with k8s service properties", args{ @@ -269,7 +327,7 @@ func TestGetDimensionUpdateFromMetadata(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, getDimensionUpdateFromMetadata(tt.args.defaults, tt.args.metadata, "-_.")) + assert.Equal(t, tt.want, getDimensionUpdateFromMetadata(tt.args.defaults, tt.args.metadata, "-_.", tt.args.stripK8sLabelPrefix)) }) } }